You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Workspace storage: Durable Object SQLite or D1 per tenant #7
Is a Durable Object per workspace, using its SQLite storage as that workspace's database, viable with Drizzle, and how does it compare to one D1 per tenant?
Read current Cloudflare and Drizzle docs and source, not memory. Establish for the Durable Object shape:
SQLite storage API, size limits, pricing, and point in time recovery.
Routing: one object id per workspace, how a request finds it, and the cost of every request crossing into the object.
WebSocket hibernation on the same object, so realtime is not precluded.
Testing inside Cloudflare's Vitest pool with real objects.
Failure modes: object stuck, migration failed mid way, cross workspace queries (the inbox spans one workspace, so probably none).
And for D1 per tenant: how a database is created on sign up through the API, how it is bound at request time, limits on database count, and what a self hoster with one workspace gains or loses. Note what changes in packages/db conventions (ULIDs, integer timestamps, no booleans) under each.
Recommend one, with the shared D1 with workspace_id as the named fallback. Findings go to a research/workspace-storage branch as a Markdown file, linked from this issue.
A Durable Object per workspace, using ctx.storage.sql, is viable with Drizzle's drizzle-orm/durable-sqlite driver — schema, migrate(), and query builder all exist and are documented.
DO SQLite storage: 10 GB per object cap, 30-day point-in-time recovery via bookmarks, storage billing starts January 2026 (row reads/writes + GB-month), and a soft throughput limit around 1,000 req/s per object.
Routing is idFromName(workspaceId): deterministic, same id always reaches the same object, no lookup table needed — this is strictly simpler than D1's per-tenant binding problem.
WebSocket Hibernation (state.acceptWebSocket) coexists with SQLite storage on the same object; hibernation stops duration billing while connections stay open, so realtime is not precluded.
@cloudflare/vitest-pool-workers exercises real Durable Objects (not mocks) via runInDurableObject, listDurableObjectIds, runDurableObjectAlarm, and evictDurableObject — including real SQLite storage.
D1 is explicitly designed by Cloudflare for "per-user, per-tenant or per-entity databases" (10 GB cap each, up to 50,000 databases/account on Paid), which is direct primary-source backing for the named fallback.
The blocker for D1-per-tenant is binding, not storage: Workers bindings are static, injected at deploy time from wrangler.jsonc/the Workers API — there is no documented runtime API to bind a new D1 database to a live Worker without a redeploy. A per-tenant D1 Worker must either redeploy on every signup or drop bindings for the D1 HTTP/REST API (POST /accounts/{id}/d1/database/{db}/query), which is a network call, not a binding read.
The import { env } from "cloudflare:workers" rule (module-scope binding read, no per-request threading) holds for D1-per-tenant only if the binding set is static; it breaks the moment tenant creation needs a new binding without redeploying.
The Durable Object shape has no such problem: env.WORKSPACE_DO is one static namespace binding read once at module scope, and idFromName(workspaceId) — computed per request from ExecutionContext/the request path, same as any per-request logic — selects the tenant. This fits the bindings rule cleanly; D1-per-tenant does not.
Recommendation: Durable Object per workspace with SQLite storage, matching decision already recorded in issue Server architecture: the walking skeleton #3's Notes; shared D1 with workspace_id stays the fallback exactly as named.
What would make the prototype fail: Drizzle's DO SQLite driver missing DDL/relational-query support found live in code (a known 2024 GitHub gap, not yet confirmed resolved), a workspace needing to cross the documented per-object 10 GB or ~1,000 req/s ceiling, or packages/db's workspace-scoping and sequence conventions not translating cleanly onto per-object storage.
Activity
hammadmajid commentedon Sep 4, 2026
Summary
ctx.storage.sql, is viable with Drizzle'sdrizzle-orm/durable-sqlitedriver — schema,migrate(), and query builder all exist and are documented.idFromName(workspaceId): deterministic, same id always reaches the same object, no lookup table needed — this is strictly simpler than D1's per-tenant binding problem.state.acceptWebSocket) coexists with SQLite storage on the same object; hibernation stops duration billing while connections stay open, so realtime is not precluded.@cloudflare/vitest-pool-workersexercises real Durable Objects (not mocks) viarunInDurableObject,listDurableObjectIds,runDurableObjectAlarm, andevictDurableObject— including real SQLite storage.wrangler.jsonc/the Workers API — there is no documented runtime API to bind a new D1 database to a live Worker without a redeploy. A per-tenant D1 Worker must either redeploy on every signup or drop bindings for the D1 HTTP/REST API (POST /accounts/{id}/d1/database/{db}/query), which is a network call, not a binding read.import { env } from "cloudflare:workers"rule (module-scope binding read, no per-request threading) holds for D1-per-tenant only if the binding set is static; it breaks the moment tenant creation needs a new binding without redeploying.env.WORKSPACE_DOis one static namespace binding read once at module scope, andidFromName(workspaceId)— computed per request fromExecutionContext/the request path, same as any per-request logic — selects the tenant. This fits the bindings rule cleanly; D1-per-tenant does not.workspace_idstays the fallback exactly as named.packages/db's workspace-scoping and sequence conventions not translating cleanly onto per-object storage.Full findings: https://github.com/kryft-dev/prismark.tech/blob/research/workspace-storage/docs/research/workspace-storage.md