Skip to main content

Install transaction

Installing an app replaces the canister's entire WebAssembly module. That is a one-way operation: once install_code in upgrade mode is dispatched, there is no rollback.

Everything in this page exists to make that safe.

The core problem

An install must change several things that live in different places:

  • the Wasm module itself (management canister)
  • the app registry at /system/apps.json
  • the Candid interface at /pkg/neutron.did
  • the stable signature
  • per-app manifests under /app/<id>/pkg/
  • web assets
  • managed memory roots, possibly migrated
  • broker registry state and reservations

If the upgrade succeeds but metadata does not, the canister runs code that disagrees with what it says about itself. If metadata is written first and the upgrade fails, the same problem inverted.

The solution: stage, journal, verify, commit

Immutable things go first. Content-addressed modules are idempotent, so uploading them early is free and a failure leaves nothing inconsistent.

Mutable things stage under a deployment id. Nothing user-visible changes yet.

The journal is the commitment record. Written before the one-way step, its client request carries the target app inventory plus the exact copy and clear plan; the Kernel snapshots the committed inventory into the journal. The obsolete-module list is a separate bounded, deployment-scoped staged file that only this journal's verified commit consumes. Install preflight checks that the migration plan, returned retirement descriptors, and the stable-signature marker all agree before any upload.

Commit requires proof. Metadata is promoted only after the freshly activated actor reports the expected fingerprints. If it reports something else, commit does not happen.

The chunked path uses at most 100 one-MiB chunks. The canister checks every returned management-store chunk hash against the client declaration, then passes the ordered hashes and whole compressed-module hash to the management install. It clears its management chunk store after verified activation (or safe failure cleanup).

Async and atomic boundaries

Uploads and staging span separate awaited calls, but their targets are inert or content-addressed. Chunk upload awaits the management canister and then rechecks that the same journal still owns the deployment before accepting the returned hash.

The self-upgrade call records its dispatch marker and queues the management install. Actor replacement can destroy the old callback, so a successful or failed call reply is not treated as activation proof; the browser polls kernel_runtime_info for the exact deployment instead.

The final kernel_install_commit path is one synchronous Motoko update with no external await. Registry and asset promotion, managed-memory retirement, broker/capability reconciliation, module collection, scheduler activation, and certification publication therefore commit together or roll back together on a trap.

Certified Assets across upgrades

An ordinary upgrade does not walk and re-hash every app-authored object. The persistent authenticated forest is validated and restored, then its combined root is republished to certified data in constant time. For retained app scopes, compiled configuration may widen numeric reservations but cannot add, remove, or reinterpret Certified Assets collections in place.

The verified install commit opens one composable certification batch around static asset promotion and Certified Assets configuration/catalog changes, and publishes the resulting root once at the end. Removing an app retires its scope and schedules bounded cleanup rather than making activation scan all of its objects.

The fail-closed interval

Between journal creation and commit, the system is deliberately unavailable rather than optimistically available.

On the canister: the predecessor actor retains its app scopes before activation. If the target actor activates while its journal is still pending, every non-kernel app scope stays inactive until commit. This briefly pauses even apps that were not being changed — and that is the intended trade. It prevents replacement code or new public ingress from executing before the target registry and assets are committed.

In the browser: once checked journal creation is confirmed, a kernel-origin cross-tab signal synchronously revokes frontend runtime state and unmounts every app frame in sibling tabs. Commit, or a proven-safe abort, sends a second signal.

Across devices: a visible tab re-observes status and runtime every 20 seconds to cover other devices and missed signals. It performs the larger registry read only when identity changed or a prior observation was uncertain.

A post-activation deployment error does not simply clear the install UI. It retains pending recovery, or an explicit frontend uncertainty fence.

Recovery

If the browser reloads mid-install, recovery resumes from the journal.

The hard question recovery must answer is: did the upgrade actually happen? Aborting an upgrade that is merely still queued would be catastrophic. Two mechanisms handle it:

  • A protected dispatch marker prevents recovery from aborting an upgrade that is queued but not yet terminal.
  • A management-queue fence permits abort only after the old actor proves the dispatched installation is terminal.

Commit independently requires the matching actor to be running. Neither check substitutes for the other.

Baseline rechecks

Every live browser app transaction uses a checked install begin, which compares the expected running deployment id before storing the journal. The repository/update session additionally re-reads its authenticated baseline before the first staging write.

Uninstall uses the same protocol

Uninstall is not a lighter operation. It:

  • keeps every live managed memory root in the target actor and migration plan;
  • removes app credentials, reservations, and broker state only in the verified commit;
  • clears retired roots synchronously inside the commit, so a trap rolls back the whole commit including journal promotion.

A provider app cannot be uninstalled while a consumer still declares it as a dependency.

Module garbage collection

A bounded deployment-scoped staging file carries the obsolete baseline module list. The verified commit consumes it; an aborted install must not have deleted modules the still-running actor imports.

What the browser and provisioner share

Both prepare the same package format and use the same compiler and actor assembler. They do not share the live app-transaction mutation helper.

The browser installer uses deployPreparedPackages for a state-preserving app transaction inside a live Neutron and carries the exact package-state snapshot used for compilation into deployment, so the Wasm and registry come from one consistent view. The provisioner compiles a complete configured package set, then uses its own whole-canister install/reinstall, seeding, receipt, and verification flow.

Deployment onStep and onProgress callbacks are observer-only; the shared helper catches and logs their failures so UI code cannot alter activation, verification, commit, or abort.