Skip to content

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:

HistoryStorageTracks
MutationsPostgres mutations tableStructural changes: creating/renaming/deleting records, granting/revoking permissions
StreamsDurable StreamsContent 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:

  1. 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 a MutationRequest
  2. Request hits POST /api/mutate
  3. Server resolves the authenticated agent, checks permissions (walk scope chain)
  4. Server calls an RPC function (insert_record_with_txid, etc.)
  5. RPC writes to records or permissions
  6. Postgres trigger fires, calls next_agent_seq(), inserts mutation row
  7. RPC returns the affected record + txid for 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 /mutations page renders the log for visibility into system behavior