~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
70 lines · 3.3 KB Raw History

Cloudflare shared storage: KV → Durable Object SQLite

Status: shipped 2026-08-05 (Worker version 37116a86). KV dropped entirely on 2026-08-11 — see below.

What changed

The Cloudflare adapter's shared storage (the SharedStoragePort: sessions, OAuth tokens/requests, DPoP jtis, app passwords, passkeys) moved out of the SHARED_KV namespace and into the Durable Object's own SQLite (state.storage.sql), alongside the repo. Implemented as createSharedStorageSql in packages/cloudflare/src/index.js, mirroring the Node adapter's schema and query logic natively (no better-sqlite3 shim).

Why

KV is an eventually-consistent, edge-cached read store (up to ~60s cache TTL per edge). For read-your-writes account state that made:

  • the "Where you're signed in" list lag after a login,
  • re-login dedup miss on a stale listSessions, leaving duplicates, and
  • the DPoP-replay check (checkAndStoreDpopJti) racy — two near-simultaneous requests could both read "not seen".

The DO's SQLite is strongly consistent and co-located, so reads see their own writes immediately. This is what Cirrus and the Node adapter already do. The account's own DID/handle were already in the DO metadata table, so identity was never affected.

Migration

On first start, migrateSharedKvToSql runs under blockConcurrencyWhile before the DO serves a request. It copies the durable KV records — actor directory, OAuth tokens, app passwords, passkeys — into SQLite once, guarded by a shared_kv_migrated flag in the metadata table. It is best-effort per row and wrapped so a failure can never abort DO startup (the flag stays unset to retry). Password sessions are intentionally not migrated — they re-login.

Note: the app-password KV key is apppass:<did>:<name>, and the DID itself contains colons, so the DID is recovered by stripping the trailing :<name> (covered by test/kv-migration.test.js).

Cleanup, 2026-08-11

The KV adapter, the one-time migration and its test, and the SHARED_KV binding in both wrangler.toml files are deleted. test/app-password-storage.test.js runs its contract suite against createSharedStorageSql on a better-sqlite3 stand-in for the DO SQL interface, in place of the KV adapter.

Two things are left to do by hand:

  1. Remove the [[kv_namespaces]] block from the deployment's wrangler.toml (in ~/code/pds). Wrangler rejects a binding the code no longer reads only when it is missing, not when it is spare, so this is tidiness.
  2. The SHARED_KV keys are untouched and still hold the pre-migration copy. Purge them with wrangler kv once the SQLite state is known good.

The migration ran and set its shared_kv_migrated flag before this cleanup, so the live deployment carries the data forward. A Worker on the published 2.1.0 package, which reads KV, starts empty when it upgrades past this point.

Validation

  • Cloudflare e2e (npm run test:e2e:cloudflare) — all auth flows pass on the SQLite path (the 7 failing consent/DPoP tests are pre-existing and identical on the KV baseline; they depend on an external appview unavailable in local wrangler dev).
  • test/kv-migration.test.js — migration key-parsing and one-shot behavior.
  • Post-deploy: resolveHandle (reads the migrated actors table) returned the account DID; identity and health intact.