The Motoko backend
The shape
A module, not an actor. The manifest's src field selects the file under
backend/.
import Memory "./memory/my_app/v1";
module {
public type AppBackendEnvironment = {
stable_memory : { my_app : Memory.Mem };
};
public class Init(env : AppBackendEnvironment) {
let mem = env.stable_memory.my_app;
public func /*update*/set_name(name : Text) : Text {
let previous = mem.name;
mem.name := name;
previous;
};
public func /*query*/get_name() : Text {
mem.name;
};
};
/*---NEUTRON GENERATED BEGIN---*/
/*---NEUTRON GENERATED END---*/
}
An app with no managed memory, backend capabilities, or app dependencies gets
Init() with no arguments instead.
Installation identity
Whenever the compiler creates an environment record, it also supplies:
installation : { network_id : Blob };
network_id is a compiler-owned, public 32-byte identity derived from the
trusted deployment root key and retained across state-preserving upgrades. It
is useful for binding network-scoped protocol objects. It is not a secret,
permission, manifest setting, or browser-provided value. Record width subtyping
lets an app omit this field when it does not consume it.
Annotations
Place the comment directly between public func and the method name:
| Annotation | Result |
|---|---|
/*update*/ | An owner-authorized update method |
/*query*/ | An owner-authorized query method |
/*internal*/ | A private generated wrapper only |
/*internal:apps*/ | Private, but exported to declared app consumers |
Method names are Motoko identifiers bounded to 128 ASCII characters.
The :unauthorized modifier is kernel-only for both query and update
methods. Ordinary apps using it are rejected; declare a
public-ingress route instead.
Requesting compiler-injected arguments
A trailing block comment inside the parameter list lists generated resources, in order:
public func /*update*/whoami(/*caller*/ caller : Principal) : Principal {
caller;
};
Everything from that comment onward is compiler-injected and omitted from the authored Candid input aliases, so injected parameters must be last. Ordinary apps may request:
callerfor a non-internal method;canister_principal, the immutable principal of the combined Neutron canister;memory_<id>for one of the app's active managed-memory roots;task_capabilitiesfor a declared scheduledasync*task that selected backend calls; orpublic_ingress_cyclesfor an exact synchronous, paid, canister-caller public-ingress update handler.
Prefer receiving memory once through Init. Actor self, foreign memory,
dependency records, provider Init instances, raw module helpers, and unknown
generated identifiers are all rejected.
Prefer async* for local chains
If one local backend function calls another asynchronous local function, return
async* T and call it with await*:
func fetchName(…) : async* Text { … };
public func /*update*/refresh_name(…) : async* Text {
let name = await* fetchName(…);
mem.name := name;
name;
};
This executes local layers inline and avoids adding commit and interleaving
points merely to move between helper functions. Keep ordinary await for the
real external call where suspension is unavoidable.
:::caution await* is not a commit point
If the computation traps before reaching a regular await, its changes roll
back to the preceding real commit boundary. That is usually what you want — but
be deliberate about it.
:::
mogen recognises the async* return and generates an await* wrapper.
Persistent state
Declare a memory root; put its type and clean-install default in an immutable schema module.
// backend/memory/my_app/v1.mo
module {
public type Mem = { var name : Text };
public func init() : Mem { { var name = "Neutron" } };
}
"memory": {
"my_app": {
"version": 1,
"schemas": { "1": { "src": "memory/my_app/v1.mo" } },
"migrations": []
}
}
Rules that matter:
- Schemas are immutable after release. Their source hash is the lineage identity.
- Schemas may import packages but not relative app modules. Otherwise editing a shared type would alter a historical stable contract.
- Runtime code imports the schema, never the reverse.
- Migrations are forward-only, bounded, and synchronous.
- A clean install calls only the target
init()— it never replays historical migrations.
Read Managed memory before designing state. It is the part of Neutron most likely to constrain a design after the fact.
Using capabilities
Two steps: declare the bound, then select the interface.
{
"capabilities": {
"stable_store": {
"api": 1,
"stores": [{ "id": "cache", "purpose": "Response cache",
"schema_version": 1, "max_entries": 1000,
"max_key_bytes": 96, "max_value_bytes": 4096,
"max_bytes": 4194304 }]
}
},
"backend": {
"capabilities": { "stable_store": { "api": 1 } }
}
}
Then name it in your local environment type:
import NeutronCapabilities "mo:neutron-capabilities";
public type AppBackendEnvironment = {
stable_memory : { my_app : Memory.Mem };
capabilities : {
stable_store : NeutronCapabilities.StableStoreV1;
};
};
Declare only what you need. Record width subtyping means the compiler's wider generated record still satisfies your narrower type.
mo:neutron-capabilities exports types only — no factories, no scope, no
authority-bearing aggregate. See
Capabilities for what each broker enforces.
Composing with other apps
Export a domain function:
public func /*internal:apps*/lookup_destination(
args : { key : Text }
) : ?Record { … };
Consume one:
"dependencies": {
"contacts": { "app": "contacts", "min_version": 100,
"functions": ["lookup_destination"] }
}
public type AppBackendEnvironment = {
app_calls : {
contacts : { lookup_destination : ({ principal : Principal }) -> ContactResult };
};
};
You receive a record of function references — never provider memory or module instances. See Backend dependencies.
Expose a public protocol
Ordinary apps cannot make a method publicly callable. Declare a route instead:
{
"capabilities": {
"public_ingress": {
"api": 1,
"routes": [{
"protocol": "example_v1",
"id": "submit",
"handler": "example_submit",
"mode": "update",
"caller": "canister",
"max_request_bytes": 8192,
"max_response_bytes": 4096,
"max_calls_per_hour": 120,
"required_cycles": 250000000
}]
}
},
"func": {
"example_submit": { "type": "update", "async": false,
"arg": ["caller", "public_ingress_cycles"] }
}
}
The handler stays a synchronous annotated function with no allow and no
expose, and its mode must match the route.
To opt into the supplemental cycles value, annotate both trailing parameters in one comment:
public func /*update*/example_submit(
input : SubmitInput,
/*caller,public_ingress_cycles*/ caller : Principal,
ingressCycles : NeutronCapabilities.PublicIngressCyclesV1
) : SubmitOutput { … };
:::caution Opting in makes the function route-only The compiler omits its ordinary owner-authorized wrapper. It becomes callable only through the paid dispatcher. If you also need an owner-authorized entrypoint, declare a separate method over shared internal logic. :::
Setting required_cycles
Treat it as a static protocol-version fact, not a caller-selected quote. It must cover every irreversible path:
- the receiver's update execution bases
- conservative measured handler instructions
- storage of the maximum admitted payload for the protocol's promised retention horizon
- margin for decoding, indexes, metadata, and future variance
The IC's cycle-cost reference is authoritative for storage and message rates.
A supplemental request is best-effort after the mutation commits — never treat it as payment for work already done. The sender separately pays the IC's inter-canister request/response base and size-dependent charges, which are not part of what you retain.
Handling ambiguity
A #revoked_after_dispatch result means the handler may have committed
before authority was revoked. Protocols should use an idempotency key or
reconcile with a read.
Things you cannot do
Your code will not compile if it constructs actors, holds actor or
shared-function references (even passed in), calls call_raw or createActor,
touches cycle primitives or (with cycles = …), uses raw stable memory or
Regions, calls certification APIs, acquires <system>, reads raw caller
attributes or canister environment, sets process-wide Candid limits, or uses raw
timers.
Each has a supported replacement. See Static checks.