HTTP and assets
A Neutron canister is also a web server. Everything the browser loads — the kernel shell, app frontends, package metadata, Motoko modules — comes out of one certified asset store.
Path conventions
| Path | Contents |
|---|---|
/ | The kernel frontend (index.html is stored at the root key) |
/app/<id>/** | An installed app's web assets |
/app/<id>/pkg/** | That app's manifest, lock, and generated schema |
/pkg/** | Kernel package metadata, including neutron.did and id.json |
/mo/<sha256>.mo | The shared content-addressed Motoko module namespace |
/system/apps.json | The app registry |
/system/install-provenance.json | Minimal source/integrity records |
/system/runtime-config.json | The certified closed runtime record |
Public by design
Committed asset paths are public. Anyone may read or probe them, and delivered bytes remain visible to IC infrastructure, controllers, gateways, and browser or intermediary caches.
Certified responses provide integrity, not confidentiality, and not installed-app inventory privacy. Sensitive app data needs its own authenticated or encrypted protocol — never the kernel static store.
Every /system/** record except apps.json, install-provenance.json, and
the runtime config remains HTTP-internal — one-time claim records, in-flight
install state — and receives the same fixed miss as an absent key.
Certified reads
Responses carry an HTTP response-certification v2 expression. The gateway verifies the response against the canister's certified tree.
A response-only wildcard certifies a miss only when no more-specific exact expression exists. A wrong-Host denial on an existing exact URL instead fails gateway verification, rather than being presented as a plausible 404.
Static assets and Certified Assets V2 share one certified root — without exposing that root, raw certificate primitives, route authority, or another app's scope to anyone.
Four things are separate claims and security review must keep them separate:
- logical admission
- physical allocator charging
- certified current state
- data confidentiality
Storage and encoding
The shared installer uses 1 MiB chunks by default, with one file operation per
update call. It normally stores compressible assets as gzip and images as
identity; content-addressed Motoko modules and explicitly created metadata
records use identity as well.
Content-addressed modules with the same path and bytes are reused. Mutable install targets must be unique, and conflicting content for a shared target is rejected.
App-scoped certified routes development
An app declares capabilities.certified_assets with API 2. The compiler derives
certified_read_routes mounts below
/app/<app-id>/_route/<mount-id>; authored http_routes remains the separate
API-1 POST surface.
The broker exposes three closed collection kinds:
| Kind | Public read profile | Mutation model |
|---|---|---|
publication | Exact Neutron Host, GET/HEAD, certified block ranges | Staged create-once and conditional delete |
immutable_blob | Portable gateway GET, immutable cache and anonymous CORS | Inline or staged create-if-absent, conditional delete |
mutable_blob | Portable gateway GET, revalidation cache and anonymous CORS | Inline compare-and-swap create/replace/delete |
Publication objects may be as large as 64 MiB and are returned as one or more
certified blocks (200 for one block; 206 for a selected block of a
multi-block object). Portable blobs are full-body, non-streaming responses of
at most 1 MiB and support neither HEAD nor Range. Success and absence are
both certified.
Apps cannot choose arbitrary status codes, headers, cache rules, CORS policy, Host authority, certificate leaves, raw paths, or cross-app scope. Staging and batch mutation use bounded idempotency and commit record state, body ownership, usage, and proof leaves atomically.
An exact installed app scope may retain records when only numeric ceilings are widened and collection and mount semantics stay unchanged. Removing the declaration retires the scope. Topology changes, semantic changes, narrowing, or a new installation scope require retirement and a fresh scope rather than preserving those records. Runtime mount disablement removes serving authority without handing its location to another app.
Anonymous certified reads do not mutate stable state, so audit records cover bounded mutation, denial, maintenance, and configuration outcomes — not a per-query serve counter.
POST update routes development
An API-1 mount claims exactly POST plus one compiler-bound internal synchronous handler.
{
"id": "receive",
"surface": "app_host",
"prefix": "/hooks/receive",
"methods": ["POST"],
"mode": "http_post_update_handler",
"handler": "receive_hook",
"max_request_bytes": 32768,
"max_response_bytes": 8192,
"max_calls_per_hour": 60,
"forward_headers": ["authorization", "content-type"]
}
What the handler sees
Only: a canonical relative path, declared headers, a bounded body, and a
digest of the mandatory Idempotency-Key. The raw key is never delivered.
It does not receive caller identity, raw Host, cookies, framing, its scope, the actor, or a durable capability handle.
Admission
The query preflight never invokes app code. http_request_update
independently rechecks authority and persists admission, then a
compiler-generated self-only wrapper rechecks the pending dispatch immediately
before synchronous app code runs.
For one hour, exact completed duplicates replay the stored reply without executing app code again; changed visible input is rejected; pending or unknown state never executes twice. Replay capacity is reserved before the handler self-call.
Anonymous gateway traffic consumes stable per-mount and app/global fixed-hour windows. A direct Candid call by a kernel-authorized principal is neither limited nor counted, though it still passes every mount, replay, concurrency, lifecycle, and cycle check.
Header handling is fail-closed
Each declared forwarded value is at most 4,096 bytes and must be unique.
Cookie/Set-Cookie, duplicate declared headers, and duplicate or non-identity
Content-Encoding reject the request rather than being silently stripped
and dispatched.
Forwarded authorization is anonymous untrusted app-protocol data unless the
handler verifies its own signed token.
The ambiguous-outcome case
The self-call commits handler state and replay before the outer POST update
resumes. If authority changes in that gap, the outer call returns 503 even
though the operation may have completed.
This is an explicit ambiguous outcome. The client should retain the same idempotency key and resolve or retry that logical operation — not mint a new key.
Retained tombstones
Changed or removed POST mounts deliberately retain bounded one-hour replay tombstones: changed same-id requests conflict, and removed mounts are unreachable. This prevents an authority change from bypassing at-most-once. Uninstall or an inactive scope purges replay state.
No sub-message instruction budget
There is no V1 instruction budget below the IC update-message limit. One admitted public handler may consume up to that platform limit despite the external-rate and concurrency bounds. Broker and handler messages are metered separately for owner-visible telemetry.
The release gate
:::caution Qualification is release-specific
The repository's Certified Assets runner exercises fresh isolated PocketIC
canisters, exact raw-query/gateway pairs, Chromium CORS, same-Wasm upgrade
persistence, hostile Range fail-closure, quota boundaries, allocator churn,
and cross-scope isolation. A checked candidate binding is only an input: a
release is qualified only when its pass-only receipt is present and validates
against the current runner, compiler, assembler, generated manifests,
implementation sources, and raw and transport Wasm.
That bounded receipt does not establish cycle cost, proof size, allocator behaviour, or upgrade safety at the 100,000-entry production ceiling. POST responses follow the gateway's update path and obtain integrity from update consensus rather than a certified query proof, so deployment qualification must also cover the deployed gateway's upgrade behaviour. :::