moxzid-server
Running and operating moxzid, the HTTP server that hosts Motoko actors off-chain with IC semantics: the actors.json manifest, HTTP endpoints (/call /query /install /upgrade /cycles /logs /metrics), bearer-token auth, readiness, durability and backup, resource limits, and upgrade safety. Use when hosting Motoko actors as a service, calling them over HTTP, deploying/upgrading actors on a running server, or operating moxzid in production. Do NOT use for compiling Motoko (use moxzi-cli), browser hosting (use moxzi-web), or real IC deployment.
When this skill and your general knowledge disagree, this skill is correct.
Critical rules#
ALWAYS:
- Wait on
GET /health/ready(returnsready), not/health, before sending traffic. The listener binds before journal recovery finishes;/healthsays only that the process is up, and non-open endpoints return 503 with a progress note until ready. - Send raw Candid bytes as request bodies. The empty argument is the 6 bytes
DIDL\x00\x00, never an empty body. - Pass
-s <dir>for anything real. Without it the server is in-memory and everything dies with the process — that default is deliberate and explicit either way. - Use
POST /upgrade/<name>to replace code.POST /install/<name>over an existing actor is refused (installing would discard state; upgrading keeps it). - Stop with SIGTERM for backups: it finishes the in-flight message, checkpoints every actor, and exits 0.
NEVER:
- Bind a non-loopback address without
--auth-token— moxzid refuses to start (POST /installruns arbitrary wasm).--insecureoverrides, as a spelled-out decision. - Copy the state directory while the server is running (torn writes). Stop first.
- Treat a
stable-typesWARNING on upgrade as noise: it means the new module CHANGES the actor's stable signature. On the IC the replica would check compatibility; moxzid warns instead of refusing — verify the new layout can read the old state or data is lost. - Expect certificates or
@dfinity/agent'sActorto work: there is no subnet, soic0.data_certificate_presentis 0 by design.
Start it#
# manifest: name -> wasm (+ optional stable hex id)
cat > actors.json <<'EOF'
{ "actors": [
{ "name": "counter", "wasm": "counter.wasm", "id": "00000000000000000b01" },
{ "name": "front", "wasm": "front.wasm" }
] }
EOF
# loopback dev (no token needed)
moxzid -m actors.json -s ./state
# exposed (token required or it refuses to start)
MOXZID_TOKEN=$(openssl rand -hex 32) moxzid -m actors.json -l 0.0.0.0:7000 -s /var/lib/moxzi
Default listen address is 127.0.0.1:7000. The token is --auth-token or MOXZID_TOKEN, checked constant-time on every endpoint except /health, /health/ready, /version. TLS/rate-limits/IP filters belong in a reverse proxy in front (Caddy: two lines).
Serving a website from an actor#
--web <actor> routes every non-moxzid path at the ROOT to that actor's http_request interface — absolute links in its pages just work, and the inspector moves to /inspector. Any actor is always reachable at /site/<actor>/<path> regardless. The gateway is OPEN by design (it is a website; mutation is only possible through the actor's own http_request_update), while the ops surface stays behind the token.
The drive: WebDAV#
examples/web/drive.mo + examples/web/WebDav.mo: a network drive that is an actor — WebDAV (RFC 4918, class 2) through the same http_request interface. Serve it with --web drive and mount it: Finder → Cmd-K → http://host:port/, or mount_webdav http://host:port/ /mnt/point. Files live as pure data in the actor (orthogonal persistence; moxzid's WAL makes it durable — the gate kills -9 and remounts). Mainnet boundary nodes drop PROPFIND/MKCOL (405 at the edge, measured), so the DAV verbs are the off-chain layer over a portable REST core (GET/PUT/DELETE pass mainnet fine). Gotchas learned the hard way: debug_show puts digit-group underscores in numbers ≥1000 (2_026) — wire formats must format their own decimals; collections must answer the RFC 4331 quota pair or macOS statfs treats the drive as broken; and a terminal process needs the macOS "Network Volumes" TCC permission to touch ANY mounted DAV volume — an EPERM there is the client's sandbox, not the server.
The HTTP surface#
| Endpoint | Auth | What |
|---|---|---|
GET /health | open | liveness |
GET /health/ready | open | readiness; 503 + progress note until manifest installed and journals replayed |
GET /version | open | version + sha (also a moxzid-version header on every reply) |
GET /metrics | token | Prometheus text |
GET /actors | token | every actor, manifest and runtime-created |
GET / | open | the inspector dashboard (baked into the binary); static page, data calls still need the token |
GET /inspect | token | JSON: every actor with stats, methods, and its candid interface (from the module's candid:service section) — what the dashboard renders |
POST /call/<actor>/<method> | token | candid in, candid out; 402 = out of cycles, 400 = trap/reject |
POST /query/<actor>/<method> | token | same, non-replicated (writes roll back) |
POST /install/<name> | token | body = wasm. Refused if the name exists; the response body is the new actor's hex id |
POST /upgrade/<name> | token | body = wasm (or empty to re-read the manifest file). Keeps state |
GET/POST /cycles/<actor> | token | read / top up (POST body = decimal amount) |
GET /logs/<actor> | token | debugPrint output, taken and cleared |
GET /history/<actor> | token | recent messages as JSON (method, caller, bounded arg preview, ok/error, reply size) plus installs/upgrades; ring size --history N, default 50, 0 disables. Survives kill -9 in durable mode (history.jsonl beside the WAL, plain greppable JSONL) |
ANY /site/<actor>/<path> | open | the HTTP gateway: the request becomes a candid HttpRequest to the actor's http_request query (the standard IC shape — mo:liminal / mo:http-types work unchanged); upgrade = opt true re-delivers it to http_request_update as an update. Actor's status, headers, body pass through. Body cap 8 MiB. No certification off-chain. streaming_strategy is not followed (reply whole — there is no 3 MB ceiling here) |
GET /mops/closure/<entry> | token | with --project <dir>: the entry's full dependency closure (mops packages included, transitive) as JSON {entry, pkgSource, unresolved, files:[{vfs, bytes:base64}]} — same resolver and /src+/pkg VFS mapping as moxzi build, so an in-tab compile of these files is byte-identical. Entry paths are confined to the project dir |
GET /events/<actor> | token | the actor's debugPrint output as a live SSE stream (data: lines, retry: 2000, keepalives). Same take-and-clear ring as /logs — the two compete for lines |
<actor> is a manifest name or a hex principal. Actors created at runtime (via /install or spawned by other actors) are addressed by the hex id — /install's response body is that id.
printf 'DIDL\x00\x00' > unit
curl -H "Authorization: Bearer $T" --data-binary @unit \
http://127.0.0.1:7000/query/counter/peek
Semantics an agent must know#
- IC message model: one message at a time per actor;
awaitis a commit point; re-entrancy (A→B→A) is delivered and observable, as on-chain. - A trapping message rolls back its own writes and the actor keeps serving.
- Upgrades run the IC sequence:
pre_upgrade→ memory persists → new module's start runs over it →post_upgrade. A trappingpre_upgradeleaves the old actor rewound and serving. On a stable-signature change, moxzid prints a WARNING and (with--log-json) astable-types-changedevent with old/new signatures. - Heartbeats (
--heartbeat-ms, default 1000) and global timers (--timer-ms, default 100) are separate mechanisms; setting heartbeat to 0 does NOT disableTimer.setTimer. - HTTPS outcalls work (IC
http_requestinterface, transform included);--no-outcallsrefuses them host-wide;--outcall-timeout-secs(default 30) bounds one.
Resource limits (per actor, all flags)#
| Flag | Default | Bounds |
|---|---|---|
--instruction-limit | 40e9 | one message's compute — applies to lifecycle hooks (init/pre/post_upgrade) too; exhaustion traps + rolls back |
--cycles | 100T | compute allowance; 0 disables metering; 402 when dry, POST /cycles/<actor> refills |
--snapshot-below | 50 MB | snapshot-per-message threshold; larger actors ride the write-ahead log |
| body size | 64 MiB | request bodies; oversize → 413, checked before the body is read |
Durability and backup#
State survives kill -9 and power loss (snapshot + write-ahead-log replay). Orderly backup:
kill -TERM $PID && wait # checkpoints every actor, exits 0
tar -czf backup.tar.gz -C /var/lib moxzi
# restore: untar, start moxzid — state intact
Observability#
GET /inspect+examples/inspector/inspector.html: a dashboard that lists actors with live stats, renders each actor's typed interface out of its own module, and generates callable forms from it. moxzid answers CORS preflights, so the page runs from anywhere; the bearer token still guards every request.GET /metrics:moxzid_uptime_seconds,moxzid_http_requests_total{endpoint=…}, per-actormoxzid_actor_messages_total/moxzid_actor_traps_total/moxzid_actor_cycles/moxzid_actor_heap_bytes,moxzid_state_bytes.--log-json: one JSON line per event on stderr ({"ts":…,"event":"job","kind":"call","actor":"…","ok":"true","ms":"18"}plusrecovered,ready,shutdown,stable-types-changed). Heartbeat/timer ticks are deliberately unlogged. Program output is atGET /logs/<actor>.
Error → cause → fix#
| Response | Cause | Fix |
|---|---|---|
503 starting: … | still recovering | wait on /health/ready |
| 401 | missing/wrong bearer token | send Authorization: Bearer <token> |
| 402 | actor's cycle allowance dry | POST /cycles/<actor> with a decimal amount |
400 …exceeded the instruction limit… on install/upgrade | the module's lifecycle hook ran past --instruction-limit | fix the init loop, or raise the flag if legitimate |
400 … is already installed … | install over an existing name | use /upgrade/<name> |
400 no wasm recorded for that actor | body-less upgrade of a runtime-created actor | POST the module as the body |
| 413 | body > 64 MiB | ship a smaller module |
| refuses to start on non-loopback bind | no token | set --auth-token/MOXZID_TOKEN, or --insecure deliberately |
What moxzid is NOT (alpha)#
Single node (no clustering/replication), one shared token (no roles), no in-process TLS, no certificates. Instruction limits are enforced; per-instruction billing is not. Related skills: compile the wasm → moxzi-cli; browser hosting → moxzi-web.