Skip to main content

Browser compilation

When you install an app, your browser compiles your operating system.

Not a server. Not a build service. Not the app author's machine. A vendored Motoko compiler, itself a WebAssembly module, runs in the kernel's page and produces the Wasm and Candid that your canister will run.

Why this is the design

Three properties fall out of it, and they are the point:

No trusted build service. There is no intermediary who could substitute different code between the package you reviewed and the module your canister runs. The bytes you approved are compiled where you are.

Compilation needs live canister state. The new actor must contain the kernel and every app already installed. Only a client that can read the current canister — its module set, its manifests, its committed versions — can assemble that correctly. Doing it anywhere else means shipping that state somewhere else.

Review and build are the same act. The install dialog can show real compile status, real Wasm size, and real diagnostics, because it is watching the actual compile that will produce the actual module.

The pipeline

Two details in that flow deserve attention.

Module bodies come over HTTP, not over a query. The authorization-protected query lists /mo/ keys only. The bodies are fetched through certified HTTP, which gets them integrity verification and the browser's cache for free. On a canister with many apps, most modules are already cached.

Version comparison happens before compilation. The browser path permits a replacement only when the target's packed release version is strictly higher than the installed one. There is no point compiling a downgrade.

Compilation is allowlist-based

compile() starts from a fresh compiler lifecycle and an empty root virtual filesystem before writing the current inputs. It then requires every config entry and every recursive import to be present in the current mofiles list before reading it.

This is a real defence, not hygiene. Without it, a module written into the virtual filesystem during a previously failed or rejected install attempt could still satisfy an import in a later build. The allowlist means a rejected package leaves nothing behind that a subsequent compile can pick up.

The danger scan

While assembling, the compiler recursively inspects each config's entry module and imports and records findings per config and per module. The vendored compiler normally returns compact import and syntax facts; the policy scanner falls back to the full AST only when those facts cannot decide safely.

RuleBehaviour
Module hash is in the reviewed whitelistFindings accepted
mo:prim / mo:⛔Ignored
Findings in an ordinary app root, memory schema root, or selected migration rootHard compile failure
Findings in kernel modulesPermitted — the explicit OS exception

The patterns checked include actor references, cyclesAdd and the cycle primitives, createActor, call_raw, stable-memory and Region APIs, certification APIs, <system>, raw caller attributes, and raw timers. See Static checks.

Practical consequences

It is isolated but still bounded. Browser compilation runs off the page thread in a dedicated Worker. Compile operations are serialized; each operation starts and ends with a fresh compiler service, and recursive inspection is disposed before a second fresh service performs final actor emission. This prevents process-global compiler state from accumulating across installs and keeps the page responsive. It does not make CPU, memory, or browser stack space unbounded: compilation still scales with the whole installed actor, not just the new app.

Your browser is in the TCB. Compilation, package validation, install review, and consent all happen in the kernel frontend. A compromised browser compromises the instance. Neutron does not claim otherwise; see Security model.

Compilation starts before approval. The install dialog shows a compiling state and keeps the accept button disabled until a compiled Wasm size exists. The final-emission Worker is disposed when compilation finishes; accepting the dialog deploys those reviewed output bytes and does not reuse a live compiler instance. Each install attempt carries an id, so a late result from a rejected attempt cannot update the current dialog.

The focused Worker regression can be run under ordinary Chromium stack settings by explicitly clearing the optional Playwright launch arguments:

PLAYWRIGHT_CHROMIUM_ARGS= NEUTRON_MOTOKO_SCALE_APPS=100 \
npx playwright test test/e2e/motoko-wasm-compiler.spec.ts --project=chromium

A pass with an explicit --js-flags=--stack-size=... argument is useful test coverage, but is not evidence that the production browser's default stack is sufficient.

The other two compile paths

The same assemble() and the same vendored compiler are used by:

The CLIneutron-cli compiles package files from disk into Wasm and Candid without a browser or a replica. Production context only, because the compiler-pinned mainnet root supplies the trusted installation identity.

The provisionerneutron-provision compiles the complete configured actor for a whole-canister install or reinstall, using the verified root context of its target.

There is also a kernel-only assembly helper that generates apps/kernel/backend/_neutron.mo from the kernel manifest alone. That file is a build-check artifact showing the wrapper shape for the kernel in isolation — it is not what runs on a canister with apps installed.

All paths share assemble(), so the wrapper template is identical everywhere. They differ only in scope and output.