Skip to content

Workspace storage: Durable Object SQLite or D1 per tenant #7

Closed

Description

@hammadmajid

Part of #3

Question

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.
  • Drizzle's Durable Object SQLite driver: schema, migrations at object start, query API, gaps.
  • 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.

Activity

  1. hammadmajid commented on Sep 4, 2026

    @hammadmajid
    CollaboratorAuthor

    Summary

    • 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.

    Full findings: https://github.com/kryft-dev/prismark.tech/blob/research/workspace-storage/docs/research/workspace-storage.md

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions