Start
Concepts
A short model of the engine. If you only read one page after the quickstart, read this one.
One domain, one clock
A domain is exactly one Durable Object and one monotonic commit counter. Every accepted mutation in that domain becomes the next commit. Within a domain, multi-slice writes are atomic and every commit is a snapshot boundary. Across domains there is no shared order.
Choose a domain boundary the way you choose a consistency boundary. Nest what must stay atomic. Mount what needs its own membership, clock, or resource envelope. A mount is a reference by domain id, not a nested subtree and not a capability.
The reducer is apply
apply(state, op, commit, shape_version, runtime?) is the only way durable state moves forward. Client and host import the same function. The host is plumbing: decode a frame, authorize, call apply, write the log, then fan out. If a concern can live outside apply, it should.
Core operations are set_field, add_child, remove_child, migrate, set_handle, extension, and stream_end. Text, points, roles, and PNG rasters are adapters or application code, not engine types.
Overlay entries are functions
The authoritative tree stays clean. Local predictions live in a separate overlay and are replayed over each new head. The client predicts its own writes, never someone else's. That is why overlay entries are functions (the pending op) rather than provisional bytes baked into the tree.
Two authority arms share the same clock:
- Server-decides — the server may reject. Permissions, uniqueness, and invariants live here.
- Server-orders — after admission, a CRDT merge does not refuse conflicts. Auth, shape, and target checks can still refuse admission.
Shapes declare policy
State is a tree of slices. Each slice has a shape: fields, optional children, an optional extension, paging (keep_last), and optional artifact coverage. The engine honors declarations. It does not know what a channel or a canvas is.
A slice's bytes are a handle: inline in the tree, elided (hash only; ask with want), external (blob key in object storage), or mounted (another domain). Presence and live stream chunks are side lanes. They do not take commit numbers.
Persist, then memory, then fanout
A domain's home is the Durable Object's SQLite. Idle objects stay there. Compaction and snapshots stay there. R2 is for External blobs — files, images, recordings — keyed from the tree. Clients fetch those blobs. There is no sleep → upload tree → wake → download path, and no second write path for quiet domains.
On the client, each pending mutation is an IndexedDB record scoped by namespace, user, and domain. Outbox.enqueue() persists before send. A mutation keeps its id across retries. Ownership lives in fields such as user_id, not in the id.
Access is application code
A client id is transport bookkeeping, never proof of identity. The host authenticates the upgrade, binds a principal to the socket, and asks application policy on input and per-recipient output. DomainServer.canDeliver() checks addressing only.
ACLs are ordinary shaped domains. Content DOs subscribe with service identities and evaluate policy locally. Clients receive private derived views such as availableActions. Those hints never authorize a mutation.
What the playground is
The playground is a consumer of these APIs, not part of the engine. It composes a chat workspace: catalog, home mounts, channels, a text document, a canvas, live streams, presence, and an admin access editor. See Playground.