Skip to main content

Project layout

A minimal app

apps/my_app/
├── backend/
│ ├── main.mo your Motoko module (manifest `src`)
│ └── memory/
│ └── my_app/
│ └── v1.mo immutable schema for memory root "my_app"
├── public/
│ ├── index.html
│ └── static/icon.png
├── src/
│ └── index.tsx frontend entry
├── test/
│ └── package.test.ts
├── build.ts frontend build script
├── mops.toml Motoko dependencies
├── neutron.json the manifest
├── neutron.lock.json generated and committed when `memory` exists
└── package.json the app's scripts

What the build produces

apps/my_app/dist/
├── web/
│ ├── index.html
│ ├── main.js
│ └── static/icon.png
├── mo/
│ └── <sha256>.mo
├── neutron.json packaged manifest, with hashed entries
├── neutron.lock.json copied lineage lock (managed-memory apps only)
└── schema.json generated JSON Schemas

apps/my_app/my_app.v0.1.0.neutron

Ownership of each file

FileOwnerCommitted?
backend/main.moYou — except the generated alias block
backend/memory/**/vN.moYou, then immutable after release
backend/memory/**/vN_to_vM.moYou
neutron.jsonYou — except the func map, which mogen writes
neutron.lock.jsonmopack, for apps with managed memory. Never hand-edityes, commit it when generated
src/**, public/**You
build.tsYou
mops.tomlYou
dist/**Generated
*.neutronGenerated
.mops/, node_modules/Tooling

Two things surprise people:

A generated neutron.lock.json is committed. mopack creates it on the first managed-memory package build. It retains schema source hashes, packaged entries, and migration closures — the artifact that proves your released v2 schema is the same v2 schema you released before. Apps without managed memory do not get a packaged lock. Losing a managed-memory lock loses the lineage proof.

Parts of files you author are generated. The manifest's func map and the Motoko alias block between the generated markers are both written by mogen. Edit the annotations, not the output.

The generated block

module {
public class Init(env : AppBackendEnvironment) {
public func /*update*/set_name(name : Text) : Text { … };
};

/*---NEUTRON GENERATED BEGIN---*/
/* generated by mogen */
public type set_name_Input = (name : Text);
public type set_name_Output = Text;
/*---NEUTRON GENERATED END---*/
}

Those aliases are what the generated actor wrapper's signature refers to. They are the app method's real authored Candid shapes — not a special ABI.

Memory directory convention

backend/memory/<memory-id>/
├── v1.mo
├── v2.mo
├── v1_to_v2.mo
├── v3.mo
├── v2_to_v3.mo
└── v1_to_v3.mo optional direct edge for skipped upgrades

A schema module may import pinned Motoko packages but not a relative app module. That restriction is what makes a released schema a stable historical contract: otherwise editing a shared Types.mo would silently change what your persisted data means.

Migration edges are different — they should import both their source and target schemas, so the compiler type-checks both sides.

These filenames are a convention, not discovery. The schemas and migrations entries in neutron.json select the actual safe relative paths.

See Managed memory.

mops.toml

Pin your Motoko dependencies explicitly.

[dependencies]
core = "https://github.com/dfinity/motoko-core#v2.6.0"

New apps use the pinned mo:core package; the official packaging path rejects direct mo:base imports. Choose collections by semantics — Map for keyed state, Set for unique membership, List for a growable random-access vector, Queue for FIFO. Keep immutable arrays for Candid vectors, fixed snapshots, static catalogs, and indexed fixed-size data. Do not mechanically replace every array.

Pinning matters especially for schemas: a schema's packaged executable entry depends on the package contents, so an unpinned dependency makes rebuilding a historical version non-deterministic.

build.ts

An ordinary frontend build. Bundle src/index.tsx to dist/web/main.js, target the browser platform, and copy public/ into dist/web.

Keep frontend code browser-safe — do not rely on Node or Bun globals at runtime. The repository's browser TypeScript config exists to catch this before you discover it inside an iframe.

If you import the design system's SCSS, add a Sass plugin to the build and link the generated CSS as a package-local asset from your HTML.

test/

TypeScript tests run under Bun. App-specific Motoko runners load neutron-motoko-wasm, compile the .mo fixtures, and execute them in the same workspace test command. See Testing, especially the migration guidance.