Skip to main content

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; the dist/ 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 pathOrdinary app (app/<id>/)Kernel package ("")
web/<path>app/<id>/<path><path>
mo/<hash>.momo/<hash>.momo/<hash>.mo
anything elseapp/<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.