# 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::`, and the DID itself contains colons, so the DID is recovered by stripping the trailing `:` (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.