Skip to main content

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

PathContents
/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>.moThe shared content-addressed Motoko module namespace
/system/apps.jsonThe app registry
/system/install-provenance.jsonMinimal source/integrity records
/system/runtime-config.jsonThe certified closed runtime record

Public by design

caution

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:

  1. logical admission
  2. physical allocator charging
  3. certified current state
  4. 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:

KindPublic read profileMutation model
publicationExact Neutron Host, GET/HEAD, certified block rangesStaged create-once and conditional delete
immutable_blobPortable gateway GET, immutable cache and anonymous CORSInline or staged create-if-absent, conditional delete
mutable_blobPortable gateway GET, revalidation cache and anonymous CORSInline 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. :::