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