Two planes, real Durable Objects. One Directory DO is the control plane (directory + sessions + RBAC + the gate); each workspace is its own Workspace DO — DB-per-tenant by construction, so no query can reach another tenant's rows.
A role expands to (action, resource) grants where resource = the workspace key, so a grant only applies in its tenant. The gate's can(account, action, workspace) matches the viewer's grants — wildcards too.
| owner | * (all actions) |
| admin | todo:createtodo:toggletodo:deletemember:manage |
| member | todo:createtodo:toggle |
GET /console → Worker → Directory DO /_console (control plane: directory + your roles) 36ms → fan-out to 3 Workspace DOs /_count (cross-tenant = N round-trips) 746ms served from colo CMH # a DATA request, e.g. GET /w/acme/todos: → Worker → Directory DO /_gate/acme → verdict{member, role} → Workspace DO non-member → 403 at the gate, before any Workspace DO is contacted
example-tenant's createTenancy(store) slices, unchanged —
the apex home, the cookie session, the transition verbs, tenant-scoped RBAC, and the
/_gate the data plane is checked against.acting as alice · Work — alice has 2 personas; switch and the workspaces below change (same human, different identities — the GitHub/Slack shape).
your workspaces
other tenants — probe one to watch the gate refuse you
Recorded per DO via instrumentStore,
newest first, refreshing every 2s — the console's own polling reads are excluded. Open a workspace or
add/check a todo and watch the queries land (the gate's membership lookup, the todos CRUD).
select id, name
from workspaces
where id = ?