Holochain Upgrade 0.6 → 0.7
For existing hApps that are currently using Holochain 0.6, here’s the guide to get you upgraded to 0.7.
There are three big changes in Holochain 0.7:
- The action model has been rewritten. An
Actionis now aheader(the fields every action shares) plus adatapayload. The per-variant action structs are gone, andEntryCreationActionis replaced byTypedAction<EntryCreationData>. This touches every integrity zome’svalidatecallback and every coordinator zome’ssignal_actionfunction, and it’s the bulk of the upgrade work. - tx5 and WebRTC have been removed. Iroh, over QUIC, is now the only network transport. The
signal_urlandwebrtc_configconductor config fields are gone. - There is no data migration path. DNA hashes change for otherwise-identical DNAs, and Holochain’s databases have been renamed. Existing installs must have their data cleared.
If your hApp is written for Holochain 0.5, follow the 0.6 upgrade guide first.
Quick instructions
To upgrade your hApp written for Holochain 0.6, follow these steps:
Update your
flake.nixto use the 0.7 version of Holochain:{ description = "Flake for Holochain app development"; inputs = { - holonix.url = "github:holochain/holonix?ref=main-0.6"; + holonix.url = "github:holochain/holonix?ref=main-0.7"; nixpkgs.follows = "holonix/nixpkgs"; flake-parts.follows = "holonix/flake-parts"; }; outputs = inputs@{ flake-parts, ... }: flake-parts.lib.mkFlake { inherit inputs; } { systems = builtins.attrNames inputs.holonix.devShells; perSystem = { inputs', pkgs, ... }: { formatter = pkgs.nixpkgs-fmt; devShells.default = pkgs.mkShell { inputsFrom = [ inputs'.holonix.devShells.default ]; packages = (with pkgs; [ - nodejs_22 + nodejs_24 binaryen ]); shellHook = '' export PS1='\[\033[1;34m\][holonix:\w]\$\[\033[0m\] ' ''; }; }; }; }{ description = "Flake for Holochain app development"; inputs = { holonix.url = "github:holochain/holonix?ref=main-0.7"; nixpkgs.follows = "holonix/nixpkgs"; flake-parts.follows = "holonix/flake-parts"; }; outputs = inputs@{ flake-parts, ... }: flake-parts.lib.mkFlake { inherit inputs; } { systems = builtins.attrNames inputs.holonix.devShells; perSystem = { inputs', pkgs, ... }: { formatter = pkgs.nixpkgs-fmt; devShells.default = pkgs.mkShell { inputsFrom = [ inputs'.holonix.devShells.default ]; packages = (with pkgs; [ nodejs_24 binaryen ]); shellHook = '' export PS1='\[\033[1;34m\][holonix:\w]\$\[\033[0m\] ' ''; }; }; }; }This will take effect later when you enter a new Nix shell. It’s important to update your Nix flake lockfile at this point, to ensure you benefit from the cache we provide:
nix flake update && git add flake.* && nix developIf your zome tests use Sweettest, one of the
holochaincrate’s build dependencies needsperlon yourPATH. If your test build fails looking for it, add it to thepackageslist above.Update your root
package.jsonwith the newhc-spinversion:{ "devDependencies": { - "@holochain/hc-spin": "^0.601.3", + "@holochain/hc-spin": "^0.700.0", "concurrently": "^6.5.1", "get-port-cli": "^3.0.0" } }{ "devDependencies": { "@holochain/hc-spin": "^0.700.0", "concurrently": "^6.5.1", "get-port-cli": "^3.0.0" } }Update your project’s package dependencies (see below).
Follow the breaking change update instructions below to get your code working again.
Update your conductor config file, if you maintain one.
Clear any existing conductor data. Holochain 0.7 can’t read databases written by 0.6, and DNA hashes have changed, so you’ll be joining a new network:
hc sandbox cleanTry running your tests:
npm testand starting the application:
npm startBe aware of some changes that won’t break your build but may affect your hApp’s runtime behavior. Read the guide at the bottom.
Update your package dependencies
Rust
Update the hdk and hdi version strings in the project’s root Cargo.toml file:
[workspace.dependencies]
-hdi = "=0.7.1"
-hdk = "=0.6.1"
+hdi = "=0.8.0"
+hdk = "=0.7.0"
[workspace.dependencies]
hdi = "=0.8.0"
hdk = "=0.7.0"
The latest version numbers of these libraries can be found on crates.io: hdi, hdk.
If your coordinator zomes have a holochain dev-dependency for Sweettest tests, its feature list needs three changes: the sqlite-encrypted feature has been replaced by encryption, wasmer_sys has been renamed to wasmer-sys-cranelift, and transport-iroh no longer exists because iroh is now compiled in unconditionally.
[workspace.dependencies]
-holochain = { version = "0.6.1", default-features = false, features = ["sqlite-encrypted", "wasmer_sys", "transport-iroh"] }
+holochain = { version = "0.7.0", default-features = false, features = ["encryption", "wasmer-sys-cranelift"] }
[workspace.dependencies]
holochain = { version = "0.7.0", default-features = false, features = ["encryption", "wasmer-sys-cranelift"] }
[dev-dependencies]
-holochain = { workspace = true, features = ["wasmer-sys-cranelift", "transport-iroh", "test_utils"] }
+holochain = { workspace = true, features = ["wasmer-sys-cranelift", "test_utils"] }
tokio = { workspace = true }
[dev-dependencies]
holochain = { workspace = true, features = ["wasmer-sys-cranelift", "test_utils"] }
tokio = { workspace = true }
A number of crates have also removed the implicit Cargo features that came from enabling an optional dependency. If any of your crates enable features on Holochain crates directly, these are the renames most likely to affect you:
| Crate | Removed feature | Use instead |
|---|---|---|
holo_hash | serde, serde_bytes | serialization |
hdi | tracing, tracing-core | trace |
holochain_integrity_types | subtle-encoding | full |
holochain_zome_types | serde_yaml | properties |
holochain_trace | tokio, shrinkwraprs | channels |
holochain_util | tokio | tokio_helper |
holochain_nonce | subtle-encoding | full |
Separately, holo_hash and holochain_zome_types no longer depend on rusqlite at all, so their sqlite and sqlite-encrypted features have been removed rather than renamed. holo_hash’s full feature no longer implies sqlite either.
Once you’ve updated your Cargo.toml you need to update your Cargo.lock:
cargo update
JavaScript
Update the client library in ui/package.json:
"dependencies": {
- "@holochain/client": "^0.20.5",
+ "@holochain/client": "^0.21.0",
// more dependencies
},
"dependencies": {
"@holochain/client": "^0.21.0",
// more dependencies
},
If you still use Tryorama for your tests rather than Sweettest, it’s community-managed at holochain-open-dev/tryorama. All the client library changes below apply to Tryorama tests too.
Then in your project’s root folder, run your package manager’s install command to update the lockfile and install the new package versions:
npm install
Update your application code
Some crate-root re-exports have been removed
If your zomes only import from hdi::prelude and hdk::prelude, you’re unaffected and can skip this.
If you import shared types by a more specific path, you may hit unresolved imports. holochain_integrity_types no longer re-exports its prelude (or Entry) at the crate root, and holochain_zome_types no longer re-exports Action/Entry at its crate root. Several holochain_zome_types modules that existed only to re-export their holochain_integrity_types counterparts — chain, countersigning, crdt, genesis, record and trace — have been removed outright, and others such as action, capability, entry, link, op and warrant no longer re-export wholesale.
Import from a prelude instead:
-use holochain_zome_types::action::Action;
-use holochain_integrity_types::Entry;
+use holochain_zome_types::prelude::Action;
+use holochain_integrity_types::prelude::Entry;
use holochain_zome_types::prelude::Action;
use holochain_integrity_types::prelude::Entry;
The action model has changed
This is the change that will require the most work. In Holochain 0.6, an Action was an enum whose variants each carried their own struct, and each of those structs repeated the fields common to all actions:
// Holochain 0.6
match action {
Action::Create(create) => {
// `create.author`, `create.timestamp`, but also `create.entry_type`
}
// ...
}
In 0.7, an Action is a struct with two fields: a header holding the fields every action shares, and a data enum holding only the fields specific to that action type.
// Holochain 0.7
match &action.data {
ActionData::Create(create) => {
// `action.header.author`, `action.header.timestamp`, and `create.entry_type`
}
// ...
}
ActionHeader holds author, timestamp, action_seq and prev_action. Everything else lives in the ActionData variant.
The ...Data structs carry the same fields their 0.6 counterparts did, minus the four header fields that were lifted out of them. So when you’re looking for a field, it either moved to header or kept its name on the data struct. Only a handful were renamed, and those are listed below.
Use this table to rename types in your zome code. Every Action variant follows the same pattern — the enum is now ActionData, matched on action.data, and each variant’s payload struct gains a Data suffix:
| Holochain 0.6 | Holochain 0.7 |
|---|---|
Action::Create(c) | ActionData::Create(c), matched on action.data |
Create, Update, Delete | CreateData, UpdateData, DeleteData |
CreateLink, DeleteLink | CreateLinkData, DeleteLinkData |
Dna, AgentValidationPkg, InitZomesComplete | DnaData, AgentValidationPkgData, InitZomesCompleteData |
OpenChain, CloseChain | OpenChainData, CloseChainData |
EntryCreationAction | TypedAction<EntryCreationData> |
action.author, action.timestamp | action.author(), action.timestamp() |
The ActionBuilder and ActionBuilderCommon builders, the NewEntryAction/NewEntryActionRef enums, and the rate_limit module have all been removed.
FlatOp variants have been renamed
The FlatOp enum you match on in your validate callback has been renamed to describe what happened rather than what the DHT does about it. The two link variants have also been folded into a single Link variant wrapping an OpLink.
| Holochain 0.6 | Holochain 0.7 |
|---|---|
FlatOp::StoreEntry(..) | FlatOp::CreateEntry(..) |
FlatOp::StoreRecord(..) | FlatOp::CreateRecord(..) |
FlatOp::RegisterUpdate(..) | FlatOp::Update(..) |
FlatOp::RegisterDelete(..) | FlatOp::Delete(OpDelete { action }) |
FlatOp::RegisterCreateLink { .. } | FlatOp::Link(OpLink::CreateLink { link_type, action }) |
FlatOp::RegisterDeleteLink { .. } | FlatOp::Link(OpLink::DeleteLink { link_type, action, original_action }) |
FlatOp::RegisterAgentActivity(..) | FlatOp::AgentActivity(..) |
FlatOp sub-types carry a TypedAction
OpEntry, OpUpdate, OpDelete, OpRecord, OpActivity and OpLink now carry a TypedAction<D> — the action’s ActionHeader paired with exactly the ActionData payload that the variant you matched guarantees — instead of a fully generic Action. You no longer have to match again to get at the data you already know is there.
TypedAction<D> dereferences to its data, so you can read the payload fields straight off the action without writing .data — action.entry_hash rather than action.data.entry_hash. You still need .data when you want to consume a field by value rather than borrow it, because you can’t move out of a deref. action.data.target_address.into_action_hash() compiles; action.target_address.into_action_hash() doesn’t.
Because that data is now directly available, the fields that used to be copied out alongside the action are gone. Read them from the action instead:
| Removed field | Read instead |
|---|---|
original_action_hash on OpRecord::UpdateEntry | action.original_action_address |
original_action_hash on OpRecord::DeleteEntry | action.deletes_address |
original_action_hash on OpRecord::DeleteLink | action.link_add_address |
base_address, target_address, tag on OpRecord::CreateLink | action.base_address, .target_address, .tag |
base_address on OpRecord::DeleteLink | action.base_address |
A DeleteLink action only records the link’s base address and the hash of the CreateLink it deletes, so its target address and tag aren’t on it. Under FlatOp::Link you don’t need to chase that yourself: OpLink has base_address(), target_address() and tag() getters that read from the original_action when the variant is a DeleteLink.
OpUpdate::original_action_hash() and original_entry_hash() remain as accessor methods. The CreateAgent and UpdateAgent variants of OpEntry, OpRecord and OpActivity still carry agent, new_key and original_key as plain fields, so you can bind them in the match arm as before.
Update your validation function signatures
Your entry validation functions take TypedAction values in place of the old action structs:
pub fn validate_create_post(
- _action: EntryCreationAction,
+ _action: TypedAction<EntryCreationData>,
_post: Post,
) -> ExternResult<ValidateCallbackResult> {
Ok(ValidateCallbackResult::Valid)
}
pub fn validate_update_post(
- _action: Update,
+ _action: TypedAction<UpdateData>,
_post: Post,
- _original_action: EntryCreationAction,
+ _original_action: TypedAction<EntryCreationData>,
_original_post: Post,
) -> ExternResult<ValidateCallbackResult> {
Ok(ValidateCallbackResult::Valid)
}
pub fn validate_delete_post(
- _action: Delete,
- _original_action: EntryCreationAction,
+ _action: TypedAction<DeleteData>,
+ _original_action: TypedAction<EntryCreationData>,
_original_post: Post,
) -> ExternResult<ValidateCallbackResult> {
Ok(ValidateCallbackResult::Valid)
}
pub fn validate_create_post(
_action: TypedAction<EntryCreationData>,
_post: Post,
) -> ExternResult<ValidateCallbackResult> {
Ok(ValidateCallbackResult::Valid)
}
pub fn validate_update_post(
_action: TypedAction<UpdateData>,
_post: Post,
_original_action: TypedAction<EntryCreationData>,
_original_post: Post,
) -> ExternResult<ValidateCallbackResult> {
Ok(ValidateCallbackResult::Valid)
}
pub fn validate_delete_post(
_action: TypedAction<DeleteData>,
_original_action: TypedAction<EntryCreationData>,
_original_post: Post,
) -> ExternResult<ValidateCallbackResult> {
Ok(ValidateCallbackResult::Valid)
}
Link validation functions collapse down to just their action arguments, because the base address, target address and tag are all reachable through the action:
pub fn validate_create_link_all_posts(
- _action: CreateLink,
- _base_address: AnyLinkableHash,
- target_address: AnyLinkableHash,
- _tag: LinkTag,
+ action: TypedAction<CreateLinkData>,
) -> ExternResult<ValidateCallbackResult> {
- let action_hash = target_address
- .into_action_hash()
- .ok_or(wasm_error!(WasmErrorInner::Guest(
- "No action hash associated with link".to_string()
- )))?;
+ let action_hash = action
+ .data
+ .target_address
+ .into_action_hash()
+ .ok_or(wasm_error!(WasmErrorInner::Guest(
+ "No action hash associated with link".to_string()
+ )))?;
let record = must_get_valid_record(action_hash)?;
// ...
}
pub fn validate_delete_link_all_posts(
- _action: DeleteLink,
- _original_action: CreateLink,
- _base: AnyLinkableHash,
- _target: AnyLinkableHash,
- _tag: LinkTag,
+ _action: TypedAction<DeleteLinkData>,
+ _original_action: TypedAction<CreateLinkData>,
) -> ExternResult<ValidateCallbackResult> {
Ok(ValidateCallbackResult::Valid)
}
pub fn validate_create_link_all_posts(
action: TypedAction<CreateLinkData>,
) -> ExternResult<ValidateCallbackResult> {
let action_hash = action
.data
.target_address
.into_action_hash()
.ok_or(wasm_error!(WasmErrorInner::Guest(
"No action hash associated with link".to_string()
)))?;
let record = must_get_valid_record(action_hash)?;
// ...
}
pub fn validate_delete_link_all_posts(
_action: TypedAction<DeleteLinkData>,
_original_action: TypedAction<CreateLinkData>,
) -> ExternResult<ValidateCallbackResult> {
Ok(ValidateCallbackResult::Valid)
}
Anywhere your validation logic reads a common action field, use the accessor for it. Action and TypedAction<D> both have author(), timestamp(), action_seq() and prev_action(), so you rarely need to reach into header yourself:
-if &action.author != record.action().author() {
+if action.author() != record.action().author() {
return Err(wasm_error!(WasmErrorInner::Guest(
"Only the author can link their own post".to_string()
)));
}
if action.author() != record.action().author() {
return Err(wasm_error!(WasmErrorInner::Guest(
"Only the author can link their own post".to_string()
)));
}
Update the validate callback body
The body of validate is scaffolded code that dispatches to the functions above, so most of this work is mechanical. The quickest way through it is to scaffold a throwaway app with the same entry and link types and copy its dispatcher across.
Two patterns in the dispatcher are worth knowing, because you’ll hit them wherever you hand-edit it.
The first is widening. In a CreateEntry arm you hold a TypedAction<CreateData>, but the validation function takes a TypedAction<EntryCreationData> so it can be shared with the update path. That conversion can’t fail, so it’s a plain .into(), where 0.6 wrapped the action in an EntryCreationAction::Create at the call site:
let action: TypedAction<EntryCreationData> = action.into();
The second is narrowing an Action you’ve fetched yourself, where you know what it must be but the type doesn’t. Each single-variant data type has a try_from_action for this, which replaces hand-matching on ActionData and rebuilding the TypedAction:
-let record = must_get_valid_record(original_action_hash)?;
-let create_link = match record.action() {
- Action::CreateLink(create_link) => create_link.clone(),
- _ => {
- return Ok(ValidateCallbackResult::Invalid(
- "The action that a DeleteLink deletes must be a CreateLink".to_string(),
- ));
- }
-};
+let record = must_get_valid_record(action.link_add_address.clone())?;
+let create_link = TypedAction::<CreateLinkData>::try_from_action(record.action().clone())?;
let record = must_get_valid_record(action.link_add_address.clone())?;
let create_link = TypedAction::<CreateLinkData>::try_from_action(record.action().clone())?;
Note that the failure case disappears rather than moving. Sys validation has already guaranteed that a DeleteLink points at a CreateLink, so a narrowing failure here means that guarantee was violated — which is a fault, not bad data from the author. Propagate it with ? instead of returning ValidateCallbackResult::Invalid, which would wrongly blame the author. TypedAction::<D>::try_from is also available if you want the WrongActionError rather than an ExternResult.
The agent activity arm keeps its agent binding, but prev_action is now an Option on the header, because the genesis Dna action has no predecessor:
-FlatOp::RegisterAgentActivity(agent_activity) => match agent_activity {
- OpActivity::CreateAgent { agent, action } => {
- let previous_action = must_get_action(action.prev_action)?;
- match previous_action.action() {
- Action::AgentValidationPkg(AgentValidationPkg { membrane_proof, .. }) => {
- validate_agent_joining(agent, membrane_proof)
- }
+FlatOp::AgentActivity(agent_activity) => match agent_activity {
+ OpActivity::CreateAgent { agent, action } => {
+ let prev = action
+ .prev_action()
+ .ok_or_else(|| wasm_error!(WasmErrorInner::Guest("expected a prior action".into())))?
+ .clone();
+ let previous_action = must_get_action(prev)?;
+ match &previous_action.action().data {
+ ActionData::AgentValidationPkg(AgentValidationPkgData { membrane_proof, .. }) => {
+ validate_agent_joining(agent, membrane_proof)
+ }
_ => Ok(ValidateCallbackResult::Invalid(
"The previous action for a `CreateAgent` action must be an `AgentValidationPkg`"
.to_string(),
)),
}
}
// ...
}
FlatOp::AgentActivity(agent_activity) => match agent_activity {
OpActivity::CreateAgent { agent, action } => {
let prev = action
.prev_action()
.ok_or_else(|| wasm_error!(WasmErrorInner::Guest("expected a prior action".into())))?
.clone();
let previous_action = must_get_action(prev)?;
match &previous_action.action().data {
ActionData::AgentValidationPkg(AgentValidationPkgData { membrane_proof, .. }) => {
validate_agent_joining(agent, membrane_proof)
}
_ => Ok(ValidateCallbackResult::Invalid(
"The previous action for a `CreateAgent` action must be an `AgentValidationPkg`"
.to_string(),
)),
}
}
// ...
}
Update signal_action in your coordinator zomes
The scaffolded signal_action function matches on the action type, so it needs the same treatment:
fn signal_action(action: SignedActionHashed) -> ExternResult<()> {
- match action.hashed.content.clone() {
- Action::CreateLink(create_link) => {
+ match &action.hashed.content.clone().data {
+ ActionData::CreateLink(create_link) => {
// ...
}
- Action::DeleteLink(delete_link) => {
+ ActionData::DeleteLink(delete_link) => {
let record = get(delete_link.link_add_address.clone(), GetOptions::default())?.ok_or(
wasm_error!(WasmErrorInner::Guest(
"Failed to fetch CreateLink action".to_string()
)),
)?;
- match record.action() {
- Action::CreateLink(create_link) => {
+ match &record.action().data {
+ ActionData::CreateLink(create_link) => {
// ...
}
// ...
}
}
- Action::Create(_create) => { /* ... */ }
+ ActionData::Create(_create) => { /* ... */ }
- Action::Update(update) => { /* ... */ }
+ ActionData::Update(update) => { /* ... */ }
- Action::Delete(delete) => { /* ... */ }
+ ActionData::Delete(delete) => { /* ... */ }
_ => Ok(()),
}
}
fn signal_action(action: SignedActionHashed) -> ExternResult<()> {
match &action.hashed.content.clone().data {
ActionData::CreateLink(create_link) => {
// ...
}
ActionData::DeleteLink(delete_link) => {
let record = get(delete_link.link_add_address.clone(), GetOptions::default())?.ok_or(
wasm_error!(WasmErrorInner::Guest(
"Failed to fetch CreateLink action".to_string()
)),
)?;
match &record.action().data {
ActionData::CreateLink(create_link) => {
// ...
}
// ...
}
}
ActionData::Create(_create) => { /* ... */ }
ActionData::Update(update) => { /* ... */ }
ActionData::Delete(delete) => { /* ... */ }
_ => Ok(()),
}
}
Record::new takes a RecordEntry
Record::new now takes a RecordEntry rather than an Option<Entry>, so that “there is no entry” and “the entry is hidden from you” are distinguishable.
match details {
- Details::Entry(details) => Ok(Some(Record::new(
- details.actions[0].clone(),
- Some(details.entry),
- ))),
+ Details::Entry(details) => Ok(Some(Record::new(
+ details.actions[0].clone(),
+ RecordEntry::Present(details.entry),
+ ))),
_ => Err(wasm_error!(WasmErrorInner::Guest(
"Malformed get details response".to_string()
))),
}
match details {
Details::Entry(details) => Ok(Some(Record::new(
details.actions[0].clone(),
RecordEntry::Present(details.entry),
))),
_ => Err(wasm_error!(WasmErrorInner::Guest(
"Malformed get details response".to_string()
))),
}
get_agent_activity takes a GetOptions
get_agent_activity now takes a fourth argument specifying how to fetch the activity, and its return type has been renamed from AgentActivity to AgentActivityStatus to resolve a name collision with the unrelated AgentActivity op variant.
-let activity: AgentActivity = get_agent_activity(
+let activity: AgentActivityStatus = get_agent_activity(
agent,
ChainQueryFilter::new(),
ActivityRequest::Full,
+ GetOptions::default(),
)?;
let activity: AgentActivityStatus = get_agent_activity(
agent,
ChainQueryFilter::new(),
ActivityRequest::Full,
GetOptions::default(),
)?;
ChainFilter uses constructors instead of a builder
ChainFilter no longer composes its limit conditions through chained builder methods. Each condition now has its own constructor that takes the chain top, so a filter carries exactly one limit condition.
-let filter = ChainFilter::new(chain_top).until_hash(oldest_hash);
+let filter = ChainFilter::until_hash(chain_top, oldest_hash);
let filter = ChainFilter::until_hash(chain_top, oldest_hash);
-let filter = ChainFilter::new(chain_top).take(10);
+let filter = ChainFilter::take(chain_top, 10);
let filter = ChainFilter::take(chain_top, 10);
ChainFilter::new, ChainFilter::until_timestamp and the include_cached_entries builder method are all still available.
must_get_agent_activity has more response variants
must_get_agent_activity now walks down the chain from the chain_top you give it and excludes any forked actions, and it reports more precisely when it can’t give you a deterministic answer. If you match on MustGetAgentActivityResponse, handle the new variants:
UntilHashMissing— theUntilHashyou asked for wasn’t found, including when it’s on a dropped fork.UntilHashAfterChainHead— theUntilHashyou asked for is later in the chain thanchain_top.UntilTimestampIndeterminate— the chain can’t be bounded deterministically by the timestamp given.IncompleteChain— the chain between the bounds isn’t fully available.
A ChainFilter with LimitConditions::Take(0) is now rejected as invalid input rather than returning an empty result.
block_agent and unblock_agent have been removed
These HDK functions are gone. Blocking is a system-level behavior driven by warrants, not something an application decides. Remove any calls to them.
Try building your zomes
Now run:
npm run build:zomes
to see if all your updated dependencies and zome code compile.
Rust client and test changes
These only apply if you use the Rust holochain_client crate, or write Sweettest tests that sign data directly.
dump_network_stats returns HolochainTransportStats
The admin and app WebSocket clients used to return different types — kitsune2_api::ApiTransportStats and kitsune2_api::TransportStats respectively, with the app response missing blocked message counts. Both now return HolochainTransportStats, which also converts Kitsune2 Space values into DnaHash values so you can use them directly.
-let stats: kitsune2_api::TransportStats = app_ws.dump_network_stats().await?;
+let stats: HolochainTransportStats = app_ws.dump_network_stats().await?;
let stats: HolochainTransportStats = app_ws.dump_network_stats().await?;
Signing traits have moved to holochain_keystore
holochain_types no longer depends on holochain_keystore, so the signing and verification extension methods that used to hang off holochain_types types now live in holochain_keystore. If you call SignedActionHashed::sign, ValidationReceipt::sign or WarrantOp::sign, import the trait from its new home:
-use holochain_types::prelude::SignedActionHashedExt;
+use holochain_keystore::SignedActionHashedExt;
use holochain_keystore::SignedActionHashedExt;
The corresponding traits for the other types are ValidationReceiptExt, WarrantOpExt and ReportEntryFetchedOpsExt. holochain_types::prelude also no longer re-exports holochain_keystore::AgentPubKeyExt.
JavaScript client changes
SignedActionHashed is no longer generic
Because there are no longer per-variant action types, SignedActionHashed doesn’t take a type parameter, and the Create, Update, Delete, CreateLink and DeleteLink types are no longer exported. If your app has scaffolded signal types, they’ll look like this:
import type {
ActionHash,
AgentPubKey,
- Create,
- CreateLink,
- Delete,
- DeleteLink,
SignedActionHashed,
Timestamp,
- Update,
} from "@holochain/client";
export type MyAppSignal =
| {
type: "EntryCreated";
- action: SignedActionHashed<Create>;
+ action: SignedActionHashed;
app_entry: EntryTypes;
}
| {
type: "LinkCreated";
- action: SignedActionHashed<CreateLink>;
+ action: SignedActionHashed;
link_type: string;
};
import type {
ActionHash,
AgentPubKey,
SignedActionHashed,
Timestamp,
} from "@holochain/client";
export type MyAppSignal =
| {
type: "EntryCreated";
action: SignedActionHashed;
app_entry: EntryTypes;
}
| {
type: "LinkCreated";
action: SignedActionHashed;
link_type: string;
};
Common action fields have moved under header
The same header/data split applies on the JavaScript side:
-const author = encodeHashToBase64(action.hashed.content.author);
-const createdAt = action.hashed.content.timestamp;
+const author = encodeHashToBase64(action.hashed.content.header.author);
+const createdAt = action.hashed.content.header.timestamp;
const author = encodeHashToBase64(action.hashed.content.header.author);
const createdAt = action.hashed.content.header.timestamp;
dumpNetworkStats returns ApiTransportStats
Both the app and admin WebSocket clients now return the same ApiTransportStats type, which nests the transport statistics under transport_stats and adds blocked_message_counts. The is_webrtc property on a connection has been renamed to is_direct.
-import { type TransportStats } from "@holochain/client";
+import { type ApiTransportStats } from "@holochain/client";
-const stats: TransportStats = await client.dumpNetworkStats();
-const connected = stats.connections.length;
-const direct = stats.connections.filter((c) => c.is_webrtc).length;
+const stats: ApiTransportStats = await client.dumpNetworkStats();
+const connected = stats.transport_stats.connections.length;
+const direct = stats.transport_stats.connections.filter((c) => c.is_direct).length;
import { type ApiTransportStats } from "@holochain/client";
const stats: ApiTransportStats = await client.dumpNetworkStats();
const connected = stats.transport_stats.connections.length;
const direct = stats.transport_stats.connections.filter((c) => c.is_direct).length;
signalingServerUrl renamed
The signalingServerUrl field in ConnectionServices is now relayServerUrl, reflecting the move to iroh relays.
Conductor config file changes
This step is only relevant if you’re working with hard-coded conductor-config.yaml files, such as when you’re building executables with the kangaroo-electron template.
Note that NetworkConfig rejects unknown fields, so a config that still sets signal_url or webrtc_config won’t just be ignored — the conductor will fail to start.
tracing_override: ~
+wasm_backend: ~
data_root_path: "###DEFINED_AT_RUNTIME###"
keystore:
type: lair_server
connection_url: "###DEFINED_AT_RUNTIME###"
admin_interfaces:
- driver:
type: websocket
port: "###DEFINED_AT_RUNTIME###"
danger_bind_addr: ~
allowed_origins: "###DEFINED_AT_RUNTIME###"
network:
base64_auth_material_bootstrap: ~
base64_auth_material_relay: ~
bootstrap_url: "###DEFINED_AT_RUNTIME###"
- signal_url: "###DEFINED_AT_RUNTIME###"
relay_url: "###DEFINED_AT_RUNTIME###"
- webrtc_config: ~
+ request_timeout_s: 60
target_arc_factor: 1
report: none
advanced: ~
-request_timeout_s: 60
-chc_url: ~
-db_sync_strategy: Resilient
+db_sync_level: Normal
tuning_params: ~
tracing_scope: ~
tracing_override: ~
wasm_backend: ~
data_root_path: "###DEFINED_AT_RUNTIME###"
keystore:
type: lair_server
connection_url: "###DEFINED_AT_RUNTIME###"
admin_interfaces:
- driver:
type: websocket
port: "###DEFINED_AT_RUNTIME###"
danger_bind_addr: ~
allowed_origins: "###DEFINED_AT_RUNTIME###"
network:
base64_auth_material_bootstrap: ~
base64_auth_material_relay: ~
bootstrap_url: "###DEFINED_AT_RUNTIME###"
relay_url: "###DEFINED_AT_RUNTIME###"
request_timeout_s: 60
target_arc_factor: 1
report: none
advanced: ~
db_sync_level: Normal
tuning_params: ~
tracing_scope: ~
The individual changes are:
signal_urlandwebrtc_configare removed, along with the tx5/WebRTC transport they configured.request_timeout_shas moved from the top level intonetwork.db_sync_strategyhas been renamed todb_sync_level, and its values have changed fromFast/ResilienttoFull/Normal/Off. The old defaultResilientmaps to the new defaultNormal;Fastmaps toOff.chc_urlhas been removed.wasm_backendis new and optional. Holochain can now be built with more than one WASM backend enabled, and this picks which one to use at runtime:"cranelift","LLVM"or"wasmi". Leave it unset to use whichever backend is available.
If you are using a local iroh relay as your relay_url, you still need to allow unencrypted relay connections:
network:
- advanced: ~
+ advanced:
+ irohTransport:
+ relayAllowPlainText: true
network:
advanced:
irohTransport:
relayAllowPlainText: true
Subtle changes
The following changes may not break your build, but they may require you to reassess whether your hApp will work as expected:
- Compiled WASM is cached in the database. Modules are compiled on demand and stored in a table in the WASM database instead of in a
wasm-cachedirectory under the data root. The directory is no longer used, and WASM is now loaded when an app is installed or enabled rather than for every installed cell at startup. - App and web-app manifests reject unknown fields. Stray or misspelled fields left over from earlier manifest schemas are now an error rather than being ignored.
hc sandboxno longer offers thewebrtcnetwork type. Onlymemandquicremain. If you have scripts that passwebrtc, update them.get_agent_activitycan returnChainStatus::Closed. This is reported when an agent’s source chain head is aCloseChainaction. It ranks aboveValidbut belowForkedandInvalid.StorageInforeports different numbers.DnaStorageInfono longer hasauthored_data_sizeorcache_data_sizefields. Source-chain data now lives in the per-DNA DHT database and is counted indht_data_size.CapAccesshas been renamed toCapAccessTypewhere it’s used as theCapGrant.cap_accesscolumn discriminant. The data-carrying grant access type used byZomeCallCapGrantkeeps theCapAccessname.- DNA migration support has been added. A new
InitPropertiestype can be set onRoleSettings::Provisionedat install time to seed a freshly migrated chain. The bytes are opaque to the conductor, are never written to the DHT, and can only be read from theinitcallback via theget_init_propertieshost function. ListAppscan filter for apps awaiting membrane proofs, via the newAppStatusFilter::AwaitingMemproofsvariant.

