Skip to content

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.

  • 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, and ctx.waitUntil has 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 an AbortSignal: once it aborts, the object stops staying awake for the work. Hooks that live outside the DO class receive this.runInBackground as an argument. It is a bound function, and the package exports its type as RunInBackground.
  • 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
  • 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.