State machines
A state machine is the computational backend for a single user.
The Zeq framework is, first, a state machine substrate — your hash-linked, Zeqond-stamped backend where computation actually runs. Sitting on top of that substrate is a new mathematical language — operators, the master equation, KO42 — that you compose against. Your compute calls come out of your state machine. Your audit entangled state is your state machine. Your encryption boundary, your API keys (zsm_*, carrying a scope, and the zeq_ak_* publish key minted alongside it) and the per-call regulatory envelopes are all bound to it.
This page is the concept. The wire-level walk-through is in Spin up your first state machine.
One machine per owner — per node
You will read "one user, one state machine" in older documents. That rule is real, but its scope is narrower than the sentence suggests. createStateMachine() opens with:
// F9: one machine per owner. If the ZID already has one, return it.
const ownerExisting = await resolveByOwner(input.ownerZid);
if (ownerExisting) return { ok: false, machine: ownerExisting, error: "owner_has_machine" };
resolveByOwner queries this node's own Postgres. Each of the eighteen nodes has its own database, so the same ZID arriving at a second node passes the check and gets a second machine. Slug uniqueness (resolveBySlug) is local for the same reason.
So: one machine per owner per node, and a ZID can own machines on several nodes. That is not a bug you should route around — GET /api/identity/home/:zid returns homes as a list and sums totals across every node that answers, precisely because this state is reachable. See your identity and your home node.
Two consequences worth internalising:
- Signing in somewhere new and finding nothing does not mean your machine is gone. It means you are on a node that does not home you. Creating another machine there gives you a second machine and splits your state across two homes. Ask the directory first.
- Genesis still converges even when the rows don't. Before minting, a node calls
readCanonicalGenesis(slug)overworm_ledger ∪ replica_ledgerand adopts the earliest recordedmachine_genesisfor that slug. A slug born anywhere on the mesh keeps one birth Zeqond network-wide — deterministic, no consensus. What does not converge is the machine's contents, which never replicate at all.
Anatomy
A state machine has six things attached to it:
- An owner ZID (
ZEQ7XXXXXXXXXXXXXXXXXXXXXXXXXX) — the equation-derived identity that created it. - A slug (
[a-z0-9-]{1,64}) — the public, URL-routable handle. Every state endpoint scopes via/:slug. - A genesis Zeqond — the integer Zeqond number at spin-up. Used as the seed for the entangled state's first
prev_hash. - A parent origin — the origin of the node that created it, read from
ZEQ_ORIGIN:const parentOrigin = String(body.parent_origin || process.env.ZEQ_ORIGIN || "zeq-dev").zeq-devis only the fallback when nothing is configured, not the value on the live fleet — machines born onzeq.mecarryzeqme, onzeqproof.comzeqproof, onzeqstate.comzeqstate. You can see it in the public aggregator:curl -s https://zeqproof.com/api/chain/aggregate/listreports"node": "zeqproof". It is what lets the framework run multi-tenant without slug clashes. - A home node — the
homeNodecolumn, set to the creating node'sZEQ_NODE_ID. A peer that later projects this identity from the replicatedmachine_genesisevent stores the same value, so exactly one node ever ticks and mints for a given machine. - A visibility flag —
is_publicexposes the read endpoints (/state,/explore,/explore/sse,/pohc/validate, the aggregator stream) without auth. Private machines require viewer role or above. - At least one API key — minted on spin-up, prefixed
zsm_, hashed at rest. Every call attributable.
The DB row for this lives in state_machines (alongside state_machine_api_keys, state_machine_roles, audit_log, contracts, contract_transitions, tally_supply, tally_tokens).
Roles
Five roles, in increasing authority:
| Role | Read | Write events | Submit transitions | Manage keys | Manage roles | Delete machine |
|---|---|---|---|---|---|---|
none | public only | — | — | — | — | — |
viewer | yes | — | — | — | — | — |
operator | yes | yes | yes | — | — | — |
admin | yes | yes | yes | yes | yes | — |
owner | yes | yes | yes | yes | yes | yes |
Roles are stored per-state-machine — the same ZID can be owner of one machine, operator of another, viewer of a third. The middlewares requireAuth (rejects anonymous) and optionalAuth (allows public reads when is_public=true) gate every state endpoint; helpers canRead(machine, zid) and canWrite(machine, zid, "operator") enforce the matrix.
API key scopes are a separate, smaller set: STATE_MACHINE_API_KEY_SCOPES = ["read", "write", "admin"], defaulting to read when a mint doesn't name one. A minted key carries exactly one scope; a read key can never write. Don't confuse the two vocabularies — roles are per-ZID on a machine, scopes are per-key.
Roles are also per-node, like everything else here: being owner on zeq.me grants you nothing at zeqproof.com, because that node has its own state_machine_roles table.
The entangled state
Every state machine has its own audit log — the rows in audit_log filtered by originId = parentOrigin:slug. Each row has:
{
id: bigserial
originId: "zeqme:my-iot-fleet" // <parentOrigin>:<slug> — the origin is the node's
zeqondNumber: bigint // when it happened, framework time
unixTimestamp: numeric // mirror in Unix seconds
phase: real in [0,1) // (unix mod 0.777) / 0.777
stateHash: char(64) hex // sha256(payload) or supplied hash
prevHash: char(64) hex // chains back to the previous row
transitionId: uuid // unique per row
transitionType: text // "event"|"state"|"file"|"connection"|"contract"|...
proofDigest: char(64) hex? // when the row carries a ZeqProof
zspEnvelope: jsonb? // when the row carries a ZSP-sealed payload
createdAt: timestamptz
}
The first row's prevHash is the genesis seed — sha256(slug || ":" || genesisZeqond). Every row after that links: prevHash[N] = stateHash[N-1]. Tampering with any row breaks the entangled state at that row's successor.
Validation is one call:
curl -sS __ZEQ_BASE_URL__/api/state/my-iot-fleet/pohc/validate \
-H "Authorization: Bearer ${ZSM_KEY}"
The handler in chain.ts walks the rows in order, recomputes each prevHash, and reports:
{ "ok": true, "valid": true, "count": 17, "broken_at": null,
"genesis_zeqond": "2287439210" }
Validation runs in time linear in the row count and is bounded by ?from=A&to=B if you want to spot-check a window.
What gets stored vs. what gets hashed
The framework runs on a hash-commitment-only doctrine for free-tier machines.
- The
event,state,file, andconnectionwrite endpoints accept either a pre-computedhash(preferred — bytes never cross the wire) or apayload(server hashes it and discards the bytes). - The entangled state row only persists the 64-char hex digest, the type metadata, the Zeqond number, and the previous hash.
- Raw bytes are NEVER stored unless the caller explicitly sends a HITE-sealed
sealed_eventenvelope — and even then, the envelope is sealed with the caller's key, not the framework's. - Logs emit a
clientIdHashand the Zeqond — never the raw ZID, IP, or payload.
This is the AT-REST-ENCRYPTION-POLICY. The framework proves that something happened at Zeqond N with payload-hash h. You keep the bytes. The entangled state is the witness.
If you want server-side bytes — for a hosted contract that needs to read the input later, for example — you opt in via the contract engine, which wraps the payload in a ZSP envelope before persisting (see Contracts).
How a state machine binds to an API key
The binding is 1:1, enforced at three layers:
- Mint.
POST /api/state/:slug/keys(or auto-mint at spin-up) writes a row instate_machine_api_keyswithstate_machine_id = machine.id. The raw key is shown once; onlysha256(raw)is persisted. - Resolve. Every authenticated route runs
authenticateKeymiddleware: it hashes the bearer, looks up the row, joins to the state machine, populatesres.locals.authZid,res.locals.authPlan,res.locals.authStateMachineId. - Enforce. Every write helper (
gateWriteinchain.ts, the contract router, the tally router) checkscanWrite(machine, zid, requiredRole)against the resolved ZID. A key minted for one machine cannot write to another, even if you put it in the wrong header.
This is why the doctrine is "every compute comes out of a state machine" — there's no privileged admin key floating outside the binding. Even self-hosted setups follow this; the dev auth-v3 flow auto-creates a state machine and an admin key on registration so you're never key-less.