Skip to content

design(id): serverless edition storage — D1 vs Turso vs both for id on Cloudflare Workers #5681

Open

Description

@dezren39

Summary

Expand #2046 (serverless edition — id on Cloudflare Workers) with the storage decision it left open: what is the backing store for id when it runs on Workers, and can the answer be more than one store?

This is a design investigation, not an implementation ticket. Its deliverable is a decision-ready comparison of Cloudflare D1 vs Turso (libSQL) vs offering both, a recommendation with the conditions that would flip it, and follow-on design tickets (see Decomposition sketch) detailed enough to be broken into implementation work without re-litigating the choice.

Relates to: #2046 (serverless edition on Cloudflare Workers), #806 (F12.13 journal replication — read replicas over a signed stream).

Why this matters

The serverless edition promises id with no server to run. That promise lives or dies on the store underneath it:

  • Every id invariant — identity, capability grants, journal / event history, world state — has to survive on a store whose consistency, replication, and failure model were designed for edge web apps, not necessarily for a signed, replicated journal.
  • Picking the wrong store does not just cost performance; it can silently weaken the journal's guarantees (ordering, durability, replay) or lock the serverless edition into a cost / limits shape that only appears at scale.
  • Conversely, supporting two stores doubles the storage-abstraction surface forever. "Both" must be justified, bounded, and priced — not adopted as an avoidance of deciding.

Options to compare

A. D1 only

  • Native to Cloudflare: Workers binding, no extra vendor, no extra credentials for the operator.
  • SQLite semantics; single-primary with read replication where available. Evaluate consistency model in detail: write consistency, read-your-writes within a session, replica lag, and what happens across regions.
  • Fit for the id journal (id: F12.13 journal replication (read replicas over a signed stream) #806): can the signed journal be stored, ordered, and replicated with the guarantees id: F12.13 journal replication (read replicas over a signed stream) #806 assumes? Where does D1 replication help, and where does the signed stream have to do its own work regardless?
  • Limits: database size, row / query limits, Time Travel / point-in-time recovery, backup / export story for an operator who wants to leave Cloudflare.

B. Turso (libSQL) only

  • libSQL as a SQLite fork with replication as a first-class feature (embedded replicas, primary replication). Evaluate the same consistency questions as D1, plus embedded-replica sync latency and offline behaviour.
  • Fit for id: F12.13 journal replication (read replicas over a signed stream) #806: does libSQL replication compose with — or conflict with — replication over a signed stream? Two replication systems moving the same data is a hazard to name explicitly.
  • Operational shape: an external dependency and account even in the "serverless" edition; self-hosted sqld as an escape hatch; what that does to the "just deploy to Workers" story.
  • Cost and limits at hobby, small-community, and large-world scales.

C. Both — per-world or per-deployment choice

  • A storage-backend abstraction with D1 and Turso as interchangeable implementations, chosen per deployment, or finer: per world.
  • Price the abstraction honestly: which id storage operations must be in the common subset, what backend-specific features are then forbidden, how migrations between backends work (export journal → replay into the other store?), and how tests / CI cover two backends without doubling flakiness.
  • Per-world choice adds a routing / metadata question: where is the "which store holds this world" fact itself stored, and what happens when a world's store is unreachable?
  • State the decision rule for when "both" is worth it (e.g. operator sovereignty, an existing Turso estate, D1 limits hit in practice) vs when it is speculative generality.

Comparison axes (required in the deliverable)

For each option, address every axis — "unknown, needs spike" is an acceptable entry, an omitted axis is not:

  1. Consistency & ordering — write consistency, cross-region behaviour, session guarantees, ordering available to the journal.
  2. Replication / journal fit (id: F12.13 journal replication (read replicas over a signed stream) #806) — how store replication interacts with signed-stream replication; single-writer assumptions; conflict and fork behaviour.
  3. Cost shape — free / entry tier, per-read / per-write / per-row pricing, storage pricing, egress; modelled at three scales (single user, small community, large public world). Include the cost of the Workers tier needed in each case.
  4. Limits & ceilings — DB size, request / query limits, connection model from Workers, cold-start and latency profile by region.
  5. Local-dev story — local D1 (Wrangler / Miniflare) vs local libSQL / sqld; can a developer run the full serverless edition offline? Seed / fixture / reset ergonomics; parity between local and deployed behaviour and known divergences.
  6. Durability, backup & exit — backup, point-in-time recovery, export in an open format, migration off the platform; data-loss windows.
  7. Security & tenancy — credentials / bindings, per-world isolation, row-level isolation assumptions, encryption-at-rest story, what a compromised Worker can reach.
  8. Operational burden — what the operator must provision, monitor, and pay for; failure modes visible to end users.

Non-goals

Acceptance criteria for the design deliverable

  • A comparison document covering options A / B / C across all eight axes above, linked from this issue.
  • A stated recommendation, the evidence behind it, and the conditions that would reverse it.
  • An explicit statement of which journal guarantees (id: F12.13 journal replication (read replicas over a signed stream) #806) each store preserves, weakens, or requires compensating design for.
  • If "both" is recommended: the common storage contract, the choice granularity (per-deployment vs per-world), and the migration path between backends.
  • Cost model at the three named scales, with assumptions listed.
  • Follow-on design tickets filed from the Decomposition sketch (or the sketch revised with reasons).

Decomposition sketch

Follow-on design tickets to be filed from this issue (indicative; revise during the work):

  1. Design: storage contract for the serverless edition — the minimal id storage interface both backends must satisfy (journal append/read, world state, indexes).
  2. Design / spike: D1 journal fit — ordering, sessions, and replication behaviour against the id: F12.13 journal replication (read replicas over a signed stream) #806 signed-stream requirements, with a small proof of concept.
  3. Design / spike: Turso / libSQL journal fit — embedded replicas and primary replication composed with id: F12.13 journal replication (read replicas over a signed stream) #806, with a small proof of concept.
  4. Design: local-dev parity for the serverless edition — Wrangler/Miniflare vs local sqld, fixtures, and the offline developer workflow.
  5. Design: backend choice & migration — per-deployment vs per-world selection, where the routing fact lives, and export/replay migration between D1 and Turso.
  6. Design: cost & limits guardrails — quotas, backpressure, and operator-visible warnings when a world approaches D1 / Turso ceilings.
  7. Design: backup, recovery & exit for serverless storage — point-in-time recovery, open-format export, and leaving Cloudflare / Turso.

Open questions

  • Does id: F12.13 journal replication (read replicas over a signed stream) #806's signed stream actually need store-level replication at all, or is store replication redundant (or actively harmful) once the journal replicates itself?
  • Is per-world backend choice a real operator need, or is per-deployment enough? What evidence would distinguish the two?
  • What is the largest world (events, state size, write rate) the serverless edition intends to support — and does either store's ceiling rule it out before cost does?
  • How much D1 / Turso behaviour can only be learned from a running spike vs vendor documentation, and which claims in the comparison are documentation-only?
  • If both are supported, who owns keeping the second backend honest — CI matrix, contract tests, or manual verification per release?
  • Does Turso's self-hosted sqld option blur the line between the serverless and self-hosted editions in a useful way, or create a third, accidental edition?

Expansion of #2046. Relates to #806.

Activity

  1. added
    priority/normalOrdinary priority (the default)
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    effort/lLarge: multi-day or open-ended; expect staged check-ins
    mode/onlineFully doable remotely: design, config, code, cloud
    kind/designA design proposal (design-architect): multi-step, phased, decision-bearing
    on Nov 4, 2026
  2. dezren39 commented on Nov 5, 2026

    @dezren39
    MemberAuthor

    Architecture direction from Drew (2026-11-04, chat):

    • Turso is the definite preference, but the answer is BOTH, with roles — not a winner-take-all pick between D1 and Turso.
    • Turso embedded replicas on endpoints: the local-first replica story is a first-class part of the design (laptop + other endpoints read locally, sync to cloud primaries).
    • The very large atlas spreads across BOTH: when the corpus is very large, shards live on Turso and D1 — the shard/fan-out architecture makes placement per-shard, so the two platforms are one logical corpus with per-shard placement by size, heat, and access pattern. (Read scaling may also be Workers-distributed per Drew's note earlier today — which is why D1's Workers-gating is not a real cost in this design.)
    • Cold tiers live elsewhere — e.g. the home lab: cold/archival shards and stores don't need cloud placement at all; the home lab (rustfs/S3-compatible storage, lab cluster) is the cold tier. Cloud is for hot and warm; cold sinks to owned hardware.

    Design consequence for this issue: decide the placement policy (what decides a shard's platform + tier, and how placement changes as a shard cools), not a single platform. The shard restore already in flight is TursoDB-first, which does not conflict: shards are independent databases, so placement can be rebalanced shard-by-shard as this policy lands.

  3. dezren39 commented on Nov 5, 2026

    @dezren39
    MemberAuthor

    Gap pass (WS-F, 2026-11-04): this issue's decomposition sketch is now filed.

    Each follow-on design ticket named in the sketch above was filed as its own issue (label app/id), so the acceptance item “Follow-on design tickets filed from the Decomposition sketch” can now be checked:

    Exception, where applicable: any sketch item already answered by a later filing is noted in the pass worklog rather than duplicated. Design prioritisation is now independent per ticket; implementation timing is a separate decision.

  4. added
    queue/queuedHas a queued task in the Supabase agent queue
    task/triageQueue packet kind: verify, reproduce, and route this item
    on Nov 5, 2026
  5. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 5, 2026
  6. 83 remaining items

  7. added
    queue/queuedHas a queued task in the Supabase agent queue
    and removed
    work/assignedQueue worker lifecycle state
    on Nov 8, 2026
  8. developing-today commented on Nov 9, 2026

    @developing-today
    Contributor

    agent-note:v1
    task: 13920
    lane: code-w
    state: handed_off

    Task 13920 picked up by lane code-w; execution lane assigned opencode. Handed off to the opencode claimant, which launches the bound session; task 13920 parked 1200s meanwhile. Issue #5681 revalidated OPEN with no hard-stop labels at pickup (laptop gh REST, 2026-11-09).

  9. dezren39 commented on Nov 9, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  10. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    queue/claimedQueue worker lifecycle state
    on Nov 9, 2026
  11. dezren39 commented on Nov 9, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

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

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)effort/lLarge: multi-day or open-ended; expect staged check-inskind/designA design proposal (design-architect): multi-step, phased, decision-bearingmode/onlineFully doable remotely: design, config, code, cloudpriority/normalOrdinary priority (the default)queue/parkedQueue task is parked (waiting on CI, time, or an event)source/design-architectFiled by the repo design architect jobstatus/parkedDeliberately paused with a recorded resume conditiontask/triageQueue packet kind: verify, reproduce, and route this itemwork/assignedQueue worker lifecycle state

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions