Package format
The archive
A .neutron file is produced from an app's dist/ directory.
From the Neutron repository root, build it through the app's complete workspace script:
npm --workspace <workspace-name> run package
The common flow validates, builds (including mogen), runs mopack, generates
method schemas, and finally runs the archive packer. Apps may add required
memory, browser, or release-evidence steps, so do not invoke pack.ts directly
as a substitute for their package script.
- Every file under
dist/is walked; thedist/prefix is not stored. - Each key is the path relative to
dist/—neutron.json,web/index.html,mo/<sha256>.mo. - Each value is that file's gzip-compressed bytes.
- The whole flat path→bytes map is MessagePack-encoded.
- The filename is derived from the manifest id and the semantic form of its
packed version, with non-alphanumeric characters in the id replaced by
_.
There is no outer archive directory and no outer gzip layer. Compression is per file, before encoding.
dist/
neutron.json
neutron.lock.json
schema.json
web/
index.html
main.js
static/icon.png
mo/
<sha256>.mo
<sha256>.mo
neutron.lock.json is present only for managed-memory apps, and schema.json
is present when the build ran the method-schema step. More generally, the
packer includes whatever the app build placed under dist/ — it does not
special-case frontend files, and there is currently no ignore-file mechanism.
Packages are neutral with respect to the destination Neutron deployment. The
removed .neutron-build.json target metadata is rejected by both the packer and
installer; runtime deployment configuration is applied only after package
preparation. A manifest's update_source remains distribution metadata and
does not bind the package to a destination Neutron canister.
Decoding is bounded and fail-closed
Before materialising any file value, the decoder preflights the MessagePack map. It bounds raw bytes, entry count, UTF-8 path length, compressed value size, one decoded entry, and total decoded bytes; it streams gzip output into a bounded buffer.
It rejects:
- duplicate or dangerous map keys (
__proto__,constructor,prototype) - unsafe paths — absolute, backslashed, or containing empty,
., or..segments - trailing MessagePack data
- multiple gzip members
- invalid gzip sizes or checksums
Limits differ by how the package arrived — see decode limits.
Install path rewriting
The installer rewrites package paths before upload, using a prefix determined by the package id.
| Package path | Ordinary app (app/<id>/) | Kernel package ("") |
|---|---|---|
web/<path> | app/<id>/<path> | <path> |
mo/<hash>.mo | mo/<hash>.mo | mo/<hash>.mo |
| anything else | app/<id>/pkg/<path> | pkg/<path> |
Each rewritten path is stored as a certified static asset with a leading /.
The special path index.html is stored at /.
For an app with id hello:
web/index.html → /app/hello/index.html
web/main.js → /app/hello/main.js
web/static/icon.png → /app/hello/static/icon.png
neutron.json → /app/hello/pkg/neutron.json
schema.json → /app/hello/pkg/schema.json
mo/<hash>.mo → /mo/<hash>.mo
The /mo/ namespace is global and content-addressed, so two apps depending on
the same module version store it once. A kernel package rewrites root assets,
which is exactly what makes kernel replacement work.
Storage encoding
Upload-time storage compression is separate from package compression. The
installer stores images and content-addressed mo/<hash>.mo modules with
content_encoding: "identity"; other assets use content_encoding: "gzip".
The shared uploader uses 1 MiB chunks by default.
dist/schema.json
Generated before the final archive step from the Motoko input/output aliases,
this file contains draft-2020 JSON Schemas for the app's public methods. It
installs to /app/<id>/pkg/schema.json.
It is a developer and package-inspection artifact. It intentionally does not duplicate the generated physical Candid name — the strict installed registry is the canonical logical-to-physical mapping — and it never authorizes binary positions or sizes. Runtime always derives the trusted type from the live installed canister and recursively validates the complete Candid value before a call.
Setup manifests are an outer protocol
A neutron-repo-v1 setup manifest does not replace or extend this format. It is
a certified outer index naming exact raw package bytes by app id, version,
SHA-256, and size.
After a package is fetched, its internal neutron.json remains
authoritative and must match the outer id and version.
Repository-provided publisher and source fields are unverified display claims;
they cannot change package behaviour or select a different fetch URL.
A repository package can never be a kernel replacement — the importer rejects
repository-delivered kernel outright. Deliberate kernel replacement remains a
manual, local-file, explicitly disclosed action.
See Repositories.