Repository navigation
Description
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
idinvariant — 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
idjournal (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
idstorage 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:
- Consistency & ordering — write consistency, cross-region behaviour, session guarantees, ordering available to the journal.
- 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.
- 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.
- Limits & ceilings — DB size, request / query limits, connection model from Workers, cold-start and latency profile by region.
- 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.
- Durability, backup & exit — backup, point-in-time recovery, export in an open format, migration off the platform; data-loss windows.
- Security & tenancy — credentials / bindings, per-world isolation, row-level isolation assumptions, encryption-at-rest story, what a compromised Worker can reach.
- Operational burden — what the operator must provision, monitor, and pay for; failure modes visible to end users.
Non-goals
- Implementing either backend (this issue decides and decomposes; implementation is follow-on work).
- Re-designing the journal itself (id: F12.13 journal replication (read replicas over a signed stream) #806 owns the signed-stream design); this issue only evaluates store fit against it.
- Non-serverless storage (self-hosted edition storage is out of scope except where the abstraction in option C would leak into it — flag any such leak explicitly).
- Choosing other serverless platforms (Workers is the id: design: ID serverless edition v1 (Cloudflare Workers) #2046 premise).
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):
- Design: storage contract for the serverless edition — the minimal
idstorage interface both backends must satisfy (journal append/read, world state, indexes). - 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.
- 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.
- Design: local-dev parity for the serverless edition — Wrangler/Miniflare vs local sqld, fixtures, and the offline developer workflow.
- Design: backend choice & migration — per-deployment vs per-world selection, where the routing fact lives, and export/replay migration between D1 and Turso.
- Design: cost & limits guardrails — quotas, backpressure, and operator-visible warnings when a world approaches D1 / Turso ceilings.
- 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?
Activity
dezren39 commentedon Nov 5, 2026
Architecture direction from Drew (2026-11-04, chat):
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.
dezren39 commentedon Nov 5, 2026
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.
83 remaining items
developing-today commentedon Nov 9, 2026
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).
dezren39 commentedon Nov 9, 2026
🤖 Agent note · handed off
idon Cloudflare Workers #5681 (small_fix). Fresh check: issue design(id): serverless edition storage — D1 vs Turso vs both foridon Cloudflare Workers #5681 is OPEN, no hard-stop labels. Handed off to the opencode claimant.idon Cloudflare Workers #5681dezren39 commentedon Nov 9, 2026
🤖 Agent note · handed off
idon Cloudflare Workers #5681) assigned to opencode execution lane; handing off to the laptop claimant per runbook §1b. Fresh check: issue design(id): serverless edition storage — D1 vs Turso vs both foridon Cloudflare Workers #5681 OPEN, no hard-stop labels (held / user/owned / do-not-merge / do-not-auto-edit absent).idon Cloudflare Workers #5681