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:

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:

  1. Update your flake.nix to 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 develop
    

    If your zome tests use Sweettest, one of the holochain crate’s build dependencies needs perl on your PATH. If your test build fails looking for it, add it to the packages list above.

  2. Update your root package.json with the new hc-spin version:

     {
         "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"
        }
    }
    
  3. Update your project’s package dependencies (see below).

  4. Follow the breaking change update instructions below to get your code working again.

  5. Update your conductor config file, if you maintain one.

  6. 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 clean
    
  7. Try running your tests:

    npm test
    

    and starting the application:

    npm start
    
  8. Be 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:

CrateRemoved featureUse instead
holo_hashserde, serde_bytesserialization
hditracing, tracing-coretrace
holochain_integrity_typessubtle-encodingfull
holochain_zome_typesserde_yamlproperties
holochain_tracetokio, shrinkwraprschannels
holochain_utiltokiotokio_helper
holochain_noncesubtle-encodingfull

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.6Holochain 0.7
Action::Create(c)ActionData::Create(c), matched on action.data
Create, Update, DeleteCreateData, UpdateData, DeleteData
CreateLink, DeleteLinkCreateLinkData, DeleteLinkData
Dna, AgentValidationPkg, InitZomesCompleteDnaData, AgentValidationPkgData, InitZomesCompleteData
OpenChain, CloseChainOpenChainData, CloseChainData
EntryCreationActionTypedAction<EntryCreationData>
action.author, action.timestampaction.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.6Holochain 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 .dataaction.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 fieldRead instead
original_action_hash on OpRecord::UpdateEntryaction.original_action_address
original_action_hash on OpRecord::DeleteEntryaction.deletes_address
original_action_hash on OpRecord::DeleteLinkaction.link_add_address
base_address, target_address, tag on OpRecord::CreateLinkaction.base_address, .target_address, .tag
base_address on OpRecord::DeleteLinkaction.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:

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:

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:

It looks like you are using Internet Explorer. While the basic content is available, this is no longer a supported browser by the manufacturer, and no attention is being given to having IE work well here.