Cloudflare deployment
The production backend for datadata today is Cloudflare Workers with Durable Objects. It’s a natural fit for the model: a folder needs exactly one authority that orders its events, and a Durable Object is exactly that — a single-threaded instance with its own storage, addressable by id.
The shape
Section titled “The shape”- One Durable Object per folder. The DO embeds the datadata server, so event ordering is free: there’s only one thread that could be ordering them.
- SQLite storage in the DO via the storage adapter — document snapshots, the append-only event log, and Yjs state, colocated with the compute.
- WebSockets to clients. Browsers connect to the DO directly; the WebSocket event bus tracks per-connection subscriptions and broadcasts to exactly the subscribers of each document.
- Auth at the door. Clients present a token (user, folder, expiry) minted by the host application; the DO validates it before accepting the connection and attaches a principal to it. What that principal may do inside the folder is then engine-enforced — see Authorization.
- Agents in the DO. Server-side agents attach as
in-process clients inside the same DO,
sharing the folder with WebSocket users via the composite event bus. A
change callback is synchronous, so an agent’s turn — minutes of model calls
— is started from it, not awaited in it. The DO base class’s
runInBackground(work, { label })is what keeps the object in memory for such work: a promise on its own is invisible to the runtime, which would hibernate the DO mid-turn, andctx.waitUntilhas no effect in a Durable Object. While work is pending, the DO keeps a timer scheduled, which rules hibernation out, and its alarm fires every 30 seconds, which rules eviction out. Each firing logs what is pending: the count, the oldest item’s age and the labels, as a warning once the oldest has been pending for five minutes. Each call names its work with a label, which its failure log carries too, and can pass anAbortSignal: once it aborts, the object stops staying awake for the work. Hooks that live outside the DO class receivethis.runInBackgroundas an argument. It is a bound function, and the package exports its type asRunInBackground. - Blobs in R2, streamed by the Worker. The Worker mounts the engine’s blob routes next to the WebSocket upgrade and holds the R2 binding: it asks the DO to authorize an upload or a download (metadata only), then streams the bytes itself, so no file body passes through the DO’s memory. One bucket serves every folder through per-folder key prefixes, and the DO’s alarm reclaims abandoned and unreferenced blobs — armed by activity, so an idle folder never wakes for it.
flowchart TB
accTitle: One Durable Object per folder
accDescr {
Browsers connect over WebSockets, presenting a token minted by the host
application which the Durable Object validates before attaching a principal
to the connection. Inside one Durable Object per folder sit the composite
event bus, the datadata server, and SQLite holding snapshots, the event log
and Yjs state. Server-side agents attach to the same bus as in-process
clients. Because one folder is one object, and one object is one thread,
event ordering needs no coordination. Blob bytes take a separate path:
browsers upload and download over HTTP through the Worker, which asks the
Durable Object only to authorize and record each transfer and streams the
bytes to and from an R2 bucket itself.
}
browsers["browsers<br/><small>WebSocket</small>"]
worker["Worker<br/><small>blob routes, HTTP</small>"]
r2[("R2<br/><small>blob bytes</small>")]
subgraph do["one Durable Object per folder"]
direction TB
bus["composite event bus<br/><small>per-connection subscriptions</small>"]
engine["datadata server"]
sqlite[("SQLite<br/><small>snapshots · event log · Yjs state</small>")]
agents["in-process agents"]
bus --> engine
engine --> sqlite
agents --> bus
end
browsers -->|"token validated<br/>at the door"| bus
browsers <-->|"upload / download"| worker
worker -->|"authorize + record<br/><small>metadata RPC</small>"| engine
worker <-->|"stream bytes"| r2
Properties that fall out
Section titled “Properties that fall out”- Region-local consistency — a folder lives where Cloudflare places its DO; all writes serialize there.
- Scale-out by folder — thousands of folders mean thousands of small, independent DOs, not one big server. The flip side: a single folder’s throughput is bounded by its single DO.
- Hibernation-friendly costs — idle folders cost nothing, and a folder with background work pending counts as busy until it settles. That keeps the instance resident without making the work durable — a deploy still ends a turn midway — so a host that marks work as in flight in a document closes out stale marks when a fresh instance starts. Each connection’s identity and subscription list ride its WebSocket’s hibernation attachment, which Cloudflare caps at 16,384 bytes — room for roughly 400 UUID-sized document ids. A connection subscribed to more than fit stays fully live, but the DO closes it on the next wake so the client reconnects and re-sends its subscriptions.