← the tour · concept

concept ix

On Cloudflare: two planes, one gate

The blessed deploy is a single Node node. Cloudflare is for convenience — and it maps orion's coordination boundary onto Durable Objects cleanly: one node = one DO.

orion runs on a single Node process with a single SQLite file. That is the default, the simple path, and the one most apps never need to leave. Cloudflare enters not as a requirement but as a platform that maps onto orion's shape: the coordination boundary is already one node, and on Cloudflare one node is one Durable Object. Nothing in the architecture changes — only what runs the loop.

The split: control plane and data plane

The multi-tenant Cloudflare deploy uses exactly two kinds of Durable Object. The Directory DO is the control plane: it holds the apex home page, cookie sessions, tenant-scoped RBAC, and the gate — the single endpoint the Worker consults before forwarding any data request. Each tenant lives in its own Workspace DO: the data plane, one SQLite file per tenant, with no query path to another tenant's rows.

Worker edge router Directory DO control plane · the gate Workspace DO · acme Workspace DO · globex Workspace DO · … verdict first →
Every data request is gated at the Directory DO before the Worker forwards it; a non-member is refused 403 at the gate, before any Workspace DO is contacted.

The gate returns a verdict before anything else moves

Binding a Workspace DO to a Worker is mechanism; membership is policy, and it lives in the Directory DO. When a request arrives for /w/acme/todos, the Worker does not go to the Workspace DO. It goes to the Directory DO's /_gate/acme endpoint first, forwarding the identity from the session. The gate runs the full RBAC check and returns a verdict — member and role, or a 403. Only on a 200 does the Worker forward to the Workspace DO, with the verdict as trusted headers. A non-member is refused before any Workspace DO is contacted.

// the Worker: gate-first, then forward — or 403 at the edge
const verdict = await directoryStub(env).fetch(
  new Request(`https://directory/_gate/${ws}`, { headers: authHeaders(request) }),
)
if (verdict.status !== 200) {
  return new Response(await verdict.text(), { status: verdict.status })
}
// member → forward with the verdict as trusted headers
const wsId = env.WORKSPACE.idFromName(ws)
return env.WORKSPACE.get(wsId).fetch(noEncoding(request, {
  "x-account": account ?? "",
  "x-role": role,
  "x-can-delete": String(canDelete),
  "x-grants": grants.join(","),
}))

The gate is not middleware. It is a real request to the control-plane Durable Object — a synchronous check with a binary verdict — and nothing downstream executes until that verdict arrives. The Workspace DO never sees a stranger.

The loop is unchanged inside the DO

Inside a Workspace DO, orion's loop runs identically to Node. The Store seam binds to ctx.storage.sql, which is synchronous — the same no-await render path the whole framework is built around. The in-process Bus works unchanged: a single-threaded DO holds every open SSE stream for its tenant, so publish("todos") fans out to every watcher the same way it does on a Node box. The same feature slices that run in Node run here; only the composition root changes.

This is the deepest reason the Durable Object model fits orion: orion's coordination boundary is already one node — and on Cloudflare, one node is one DO. The bus never needs an external pub/sub, because the coordinator is already singular by construction.

Trade-offs — be honest about them

The Cloudflare deploy is a real option, not a caveat-free one. Three limits worth knowing before you commit:

Keeping SSE cheap: the Worker–DO broker planned

The SSE-residency cost above has a clean fix that keeps Datastar's SSE end to end: a broker. Split who holds what. The Worker — stateless, and not billed for idle wall-time the way a resident DO is — holds the long-lived SSE to the browser. The Worker then talks to the Workspace DO over a WebSocket, which can hibernate. On each event the DO wakes, renders the fragment, pushes it up the socket to the Worker (which relays it down the SSE to the user), and goes back to sleep after its idle timeout.

browser Datastar · SSE Worker holds the SSE · cheap Workspace DO hibernates SSE WS (hibernatable)
The open connection to the user costs Worker time, not DO time — so the DO idles to zero between events.

Planned — the deployed demo keeps it simple (the DO holds the SSE directly), which is exactly why an idle workspace DO stays resident. The broker is the production cost-optimization; it changes only where the stream is held, not the loop.

The architecture console

The running deploy exposes a /console that X-rays the whole design live: both planes, every Workspace DO's real idFromName id and live todo count (a genuine cross-tenant fan-out — N DO round-trips), the RBAC role-to-permission map, a persona switcher, and a live SQL feed showing the actual store calls as they land across Durable Objects. Switch to alice·Personal, who owns home but is not in acme, then probe acme — the gate returns a 403 toast before any Workspace DO is contacted. "No cross-tenant leak by construction," clickable.

The todos page itself shows the x-ray footer: which physical DO served it, the viewer's role, the RBAC grant strings, and the live permission check result. A member with the member role sees no delete control; the Workspace DO's command refuses it regardless. The button is a courtesy; the gate is the law.

It is the same architecture described on this page — made visible, running on Cloudflare, open for inspection.

orion ✦ a belt of stars · built on datastar