Mutations
Mutations are the append-only audit log of every structural change. They answer “what happened, by whom, in what order” for everything except message content (which lives in durable streams).
Two histories
Two kinds of history, stored separately:
| History | Storage | Tracks |
|---|---|---|
| Mutations | Postgres mutations table | Structural changes: creating/renaming/deleting records, granting/revoking permissions |
| Streams | Durable Streams | Content within a scope: messages, events, anything that accumulates chronologically |
They don’t overlap. A message posted in a room never touches the mutations table. A room being created never touches a durable stream.
Schema
mutations ( id uuid primary key, seq bigint not null, -- monotonic per agent (logical clock) record_id uuid references records(id), action text not null, -- 'insert', 'update', 'delete' payload jsonb, -- state change or snapshot agent_id uuid references records(id), created_at timestamptz)How mutations are created
Nobody writes to the mutations table directly. Triggers on records and permissions fire on every insert, update, or delete, producing a mutation row automatically.
The write path:
- Client calls a builder from
packages/core/mutations/(createHouse,grantPermission, etc.). Builders validate inputs against Zod record-content schemas (packages/core/schemas/record-content.ts) and produce aMutationRequest - Request hits
POST /api/mutate - Server resolves the authenticated agent, checks permissions (walk scope chain)
- Server calls an RPC function (
insert_record_with_txid, etc.) - RPC writes to
recordsorpermissions - Postgres trigger fires, calls
next_agent_seq(), inserts mutation row - RPC returns the affected record +
txidfor Electric reconciliation
Ordering
seq is the ordering primitive — monotonic per agent, generated by next_agent_seq(). It’s a logical clock, not a wall clock. created_at is informational only.
This means you can reconstruct what a specific agent did in order, and interleave multiple agents’ sequences to reconstruct global history.
Payload
- insert: captures the new record’s fields (type, parent_id, author_id, content)
- update: captures the updated state
- delete: snapshots the full record before deletion (so state can be reconstructed without the original row)
Permission mutations
Permission changes (grant, revoke, mode change) also produce mutation rows. The trigger on permissions uses grantor_id as the acting agent. record_id is null for permission mutations — the affected scope is in the payload.
What mutations enable
- Audit trail: who did what, when, in what order
- Undo: replay mutations up to time T to reconstruct structural state
- Debugging: inspect the sequence of changes that led to current state
- Introspection: the
/mutationspage renders the log for visibility into system behavior