chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
# 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.