Documents & folders
Documents
Section titled “Documents”A document is the unit of data:
docId— a string id, unique within its folder.type— a string naming the document type. Every type has a schema document (sys:schema:<type>) that writes are validated against.sequence— a monotonically increasing number assigned by the server on every accepted change. It orders the document’s log and is what a sequence guard anchors to. It counts JSON-lane changes only — edits to embedded Yjs documents travel on a separate lane and don’t advance it.data— the JSON payload. Rich text fields hold references to embedded Yjs documents rather than raw text.
A document can also have a user-facing name — an optional display label that
is index-level metadata, not part of data. Set it at create time or change
it later with renameDocument; renaming never touches the document’s data,
sequence or event log. A name is capped at 1024 characters; an empty or
whitespace-only name is treated as unnamed (null). By convention a / in the
name nests documents into
display-only subfolders — a UI grouping over the flat label, not an engine-level
hierarchy (the folder below is still one flat namespace).
Folders
Section titled “Folders”A folder is the unit of synchronization and authority: one server instance owns one folder, orders all of its events, and broadcasts to all of its subscribers. In the current deployment a folder maps to one Cloudflare Durable Object.
Clients subscribe per document, not per folder — a client only receives changes for documents it has subscribed to.
Schemas are per folder, too: a document type is defined by the
sys:schema:<type> document living inside the folder, and a document’s
schema_sequence is a folder-local coordinate. Two folders can carry the same
type name at independently evolved schemas.
Admission is folder-granular — a connection presents a host-minted token
to enter the folder — but inside it the engine authorizes every write and
filters every read: declarative role rules per document type evaluated against
the folder’s sys:access document, hard scope caps on the principal, and app
policy callbacks for per-document rules. See
Authorization.
Deleting documents
Section titled “Deleting documents”Deletion is soft: a deleted document leaves the index and every read path
but is listed in sys:trash, with its row, event
log, and embedded Yjs state retained so it can be restored. Delete is a
lifecycle change, recorded in the folder’s membership log rather than the
document’s own event log — restoring a document returns it at the exact sequence
it left, history intact.
Deletion is also observable. Live subscribers are pushed a doc:deleted
event the moment a delete commits (an open editor can show “this document was
deleted” instead of silently going stale), and subscribing to an
already-deleted document answers doc:deleted too — deliberately distinct
from “never existed”. Subscriptions survive the deletion, so a restore pushes
the document straight back to everyone who was watching.
A deleted document still owns its id: creating a new document under a
deleted docId is rejected with a distinct deleted error (restore it, or pick
a new id) rather than the benign already-exists signal.
Deletes are optimistic and stackable: the document reads as gone locally the
moment you call deleteDocument, and you can delete a document whose own create
is still in flight — the pair goes to the server in author order rather than
being cancelled against each other locally, so both writes report their real
outcome. If the create is rejected, the trailing delete is dropped along with it
and the failure surfaces once.
Purge is the second stage — hard deletion, for storage reclamation and
right-to-erasure. It takes a deleted document, destroys its event logs, Yjs
state and snapshot, and releases its id: afterwards the id behaves exactly
like one that never existed — a subscribe answers notfound, restore has
nothing to resurface, and a create under it simply starts a fresh document.
That release is what makes purge safe for deterministic ids (one document per
domain entity, a settings singleton): an id is never permanently burned. One
erasure boundary to know: purge destroys a document’s content, never its
identifier — the original id is kept in a server-side audit record, so don’t
encode data in a docId that would itself need erasing.
Purge has two entry points, and both are batch-shaped and all-or-nothing:
one storage transaction, one sys:trash sequence step for the whole batch,
and a failing member anywhere destroys nothing. Clients purge over the wire
with purgeDocuments (the “delete forever” button) — but only for document
types whose schema explicitly opts in with an access.purge rule; the
default is nobody, admins included (see
Authorization). It is deliberately never
optimistic and never queued offline: irreversible destruction doesn’t sit in
a replay buffer. On the server, purgeDocuments (system authority) empties
specific tombstones, and purgeDeletedDocuments sweeps everything deleted
before a cutoff, oldest first and optionally bounded — the mechanism behind
“trash empties after 30 days”. The retention window and the trigger (a Durable
Object alarm, a cron, opportunistically on wake) belong to the application;
datadata deliberately owns no scheduler.
One carve-out: sys: documents — including schema documents — can’t be
deleted, and therefore can’t be purged; see
Limitations.
System documents
Section titled “System documents”The engine maintains a handful of documents itself — the folder index, the trash, schemas, staged-work views, presence — each readable and subscribable like any other document. System documents enumerates them.