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
| File | Owner | Committed? |
|---|---|---|
backend/main.mo | You — except the generated alias block | ✅ |
backend/memory/**/vN.mo | You, then immutable after release | ✅ |
backend/memory/**/vN_to_vM.mo | You | ✅ |
neutron.json | You — except the func map, which mogen writes | ✅ |
neutron.lock.json | mopack, for apps with managed memory. Never hand-edit | ✅ yes, commit it when generated |
src/**, public/** | You | ✅ |
build.ts | You | ✅ |
mops.toml | You | ✅ |
dist/** | Generated | ❌ |
*.neutron | Generated | ❌ |
.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.