Storage & event log
The server persists through a storage adapter; the production implementation is SQLite inside each folder’s Durable Object. The layout is simple and worth knowing because it explains several behaviors.
Adapters come in two flavors, one engine. A synchronous adapter
(in-memory, Durable Object SQLite) backs DatadataServer, whose operations
complete on the caller’s stack. An asynchronous adapter — one whose
methods return promises, like a Postgres driver — backs AsyncDatadataServer,
the Promise-returning twin: the same engine code executes every operation
(the internals are written once as storage-agnostic generators), with the
async server adding an internal FIFO so operations still run one at a time,
in call order, however long each storage round-trip takes. A synchronous
adapter satisfies the async interface as-is.
The FIFO’s scope is one server, and one server serves one folder. A host that
serves many folders from one process — many folders over one Postgres
database — runs one async server per folder, owned by a FolderRegistry: it
guarantees exactly one live server (and storage adapter) per folder, keeps
different folders’ operations from serializing against each other, and at
shutdown drains every folder’s queue before the host closes the shared
database.
What’s stored
Section titled “What’s stored”- Document snapshots — current JSON, type, and sequence per document.
Reads and
doc:initare served from here; nothing is replayed. - The JSON event log — every accepted structured change, appended as its
JSON Patch with its sequence number and
the attribution of whoever wrote it. This is the document’s history: an
audit trail, the source for
doc:get-events, and the raw material for future history features (diffs over time, undo, blame). - The Yjs lane — current Yjs binary state per embedded text document, plus
its own append-only update log under its own per-document version,
yjs_sequence. It mirrors the JSON lane exactly: the document’syjs_sequenceis its latest Yjs event’s, just assequenceis its latest JSON event’s. Each stored update carries its own attribution; a Y.Doc deletion is a tombstone row. The two lanes are independent: a Yjs update never touchessequence, the JSON log, or the snapshot.
Attribution
Section titled “Attribution”Every stored event, on both lanes, records the principal that wrote it:
subject— the principal’s subject, the user or service on whose behalf the write happened. Null for an anonymous write.actor— what kind of code was acting:user(a frontend acting for the user),agent:<name>(AI-generated), orsys:<name>(server-side system code, e.g.sys:schema-seed).
The two are independent, and neither confers privilege — identity is never authority. Together they make “which edits did the AI make?” a question you answer from the data rather than from application logs.
Attribution is written once and never rewritten. An imported event keeps its original author’s attribution rather than the importer’s, which is what lets a replayed stream still name the author of each event. It’s also why a write-behind burst stays single-author: a different author’s delta closes the open burst and starts a new one. Events stored before the authorization layer existed carry a null actor.
Attribution is durable and belongs to accepted history. Staged changes carry no authorship of their own — staging is transient (the changeset clears at commit), so authorship is recorded here, on the committed event, not on the staged change; see Changesets & lanes.
Consequences
Section titled “Consequences”- Reads are O(document), not O(history). Snapshots mean a subscriber’s init cost doesn’t grow with a document’s age.
- History is first-class. Because changes are stored as patches (not opaque blobs), history is inspectable — by devtools, by audit, by agents — and replayable elsewhere: see Portable event streams.
- Writes are serialized per folder. One Durable Object, one writer — the storage layer never sees concurrent writes to a folder. A multi-folder host gets the same property from one async server per folder; only different folders’ statements interleave, and every statement is folder-scoped.
- The logs are the recovery authority. A document’s stored data is, by invariant, equal to the replay of its own logs — the JSON data from the patch log, each Y.Doc from the fold of its Yjs log. Snapshots are served for speed but are reproducible, and verify/rebuild/export/import tooling exists against that guarantee: a document export ships both lanes, and replay folds each lane in its own order. (Even schema migrations applied on read are frozen into the log as ordinary events, so replay never re-derives them.)
Write-behind for streamed text
Section titled “Write-behind for streamed text”The Yjs lane is also where the one deliberate durability relaxation lives.
Streamed CRDT deltas — an AI typing into a Y.Doc, a fast human — would mean
a storage write per keystroke, so the production adapter buffers a burst
in memory and flushes it as one merged transaction into the Yjs log. A
burst is single-author, so the merged rows keep correct attribution. The fold
shortens the log rather than perforating it: the merged-away updates are
never written, so the surviving rows take the next positions and stay
contiguous, and the document’s yjs_sequence follows the log down with them.
Coalescing is therefore invisible from outside storage — nobody can count how
many keystrokes went into a merged row, and nobody needs to. The JSON lane never
does this: data changes keep strict persist-before-broadcast ordering.
The cost is a bounded window where a burst has been broadcast but not yet stored. A crash inside it loses the burst atomically (disk reverts to the pre-burst state; the document stays self-consistent) — but no longer permanently: any client that saw the broadcasts pushes the missing content back on its next reconnect.