Summary
The encryption migration journal system (services/storage/encryptionMigrationJournal.ts, protectedStoreMigration.ts, secondaryProtectedStoreAdapters.ts) currently has zero production callers — beginEncryptionMigration, runProtectedStoreMigration, and getRegisteredSecondaryProtectedStoreAdapters are only invoked from tests/unit/storage/*.test.ts. The live "Encrypt project data at rest" toggle (components/settings/PrivacySection.tsx) only supports enable (setupIdbEncryption) and unlock (verifyAndInitIdbEncryption) — PassphraseModalMode is type-restricted to 'set' | 'unlock' only, and the toggle is explicitly disabled={encEnabled} once on ("Locked is still encrypted; showing it as off invited an unsafe disable path.").
This matches documented tech debt in CLAUDE.md § Known Technical Debt, B-1: "Actual IDB read/write integration for stores is Phase 4 (service-layer only currently)."
Because there is no production trigger, three CodeRabbit findings from PR #337's review are real bugs in the migration engine's own logic, but not currently reachable by any user action. They were deliberately left unresolved (not fixed, not falsely closed) on that PR pending this dedicated Phase-4 design work. This issue is the tracking follow-up CodeRabbit requested (PR #337 thread acks: "A follow-up issue should track the Phase-4 production migration flow, including UI entry points, journal startup, adapter checkpoint creation, recovery UX, and verification.").
Required Phase-4 scope
- Disable + passphrase-rotation product flows. Extend
PassphraseModalMode beyond 'set' | 'unlock', build the UI entry points, and wire a real production caller for beginEncryptionMigration/runProtectedStoreMigration.
- Wire secondary adapters into production migration startup (originally flagged at
services/storage/secondaryProtectedStoreAdapters.ts#L136-185). No production code currently calls getRegisteredSecondaryProtectedStoreAdapters; migration checkpoints must be created with IDs matching each adapter's databaseName/storeName format (sceneRevisionAdapterSpec, inferenceCacheAdapterSpec), otherwise those stores are silently never migrated.
- Atomic migration admission vs. protected writes (originally flagged at
services/storage/idbAssetStore.ts#L32-34 + 6 sibling sites). assertNoActiveEncryptionMigration() is only a preflight check — journal ownership is stored separately from the affected data stores, so a writer can pass the check, migration can claim ownership and commit, and the writer can then persist plaintext/ciphertext under the old key. Needs shared cross-tab write admission: migration ownership must block new writers and wait for admitted writers; writers must acquire admission before key resolution and hold it through the write transaction. Applies to saveImage(), saveBinderAsset(), deleteAllBinderAssetsForProject(), saveStoryCodex(), saveRagVectors(), saveSlice(), createSnapshot().
- Handle records deleted during verification (originally flagged at
services/storage/protectedStoreMigration.ts#L280). verify() counts current records, but checkpoint.processed counts records seen during migration. Cache eviction (services/ai/aiInferenceCacheService.ts) and scene-revision retention (services/sceneRevisionService.ts) can delete records independently of migration state and cause a false ProtectedStoreVerificationShortfallError/recovery-required. Either block those two mutation paths during migration, or verify against the set of records that still exist.
Acceptance criteria
References
Summary
The encryption migration journal system (
services/storage/encryptionMigrationJournal.ts,protectedStoreMigration.ts,secondaryProtectedStoreAdapters.ts) currently has zero production callers —beginEncryptionMigration,runProtectedStoreMigration, andgetRegisteredSecondaryProtectedStoreAdaptersare only invoked fromtests/unit/storage/*.test.ts. The live "Encrypt project data at rest" toggle (components/settings/PrivacySection.tsx) only supports enable (setupIdbEncryption) and unlock (verifyAndInitIdbEncryption) —PassphraseModalModeis type-restricted to'set' | 'unlock'only, and the toggle is explicitlydisabled={encEnabled}once on ("Locked is still encrypted; showing it as off invited an unsafe disable path.").This matches documented tech debt in
CLAUDE.md§ Known Technical Debt, B-1: "Actual IDB read/write integration for stores is Phase 4 (service-layer only currently)."Because there is no production trigger, three CodeRabbit findings from PR #337's review are real bugs in the migration engine's own logic, but not currently reachable by any user action. They were deliberately left unresolved (not fixed, not falsely closed) on that PR pending this dedicated Phase-4 design work. This issue is the tracking follow-up CodeRabbit requested (PR #337 thread acks: "A follow-up issue should track the Phase-4 production migration flow, including UI entry points, journal startup, adapter checkpoint creation, recovery UX, and verification.").
Required Phase-4 scope
PassphraseModalModebeyond'set' | 'unlock', build the UI entry points, and wire a real production caller forbeginEncryptionMigration/runProtectedStoreMigration.services/storage/secondaryProtectedStoreAdapters.ts#L136-185). No production code currently callsgetRegisteredSecondaryProtectedStoreAdapters; migration checkpoints must be created with IDs matching each adapter'sdatabaseName/storeNameformat (sceneRevisionAdapterSpec,inferenceCacheAdapterSpec), otherwise those stores are silently never migrated.services/storage/idbAssetStore.ts#L32-34+ 6 sibling sites).assertNoActiveEncryptionMigration()is only a preflight check — journal ownership is stored separately from the affected data stores, so a writer can pass the check, migration can claim ownership and commit, and the writer can then persist plaintext/ciphertext under the old key. Needs shared cross-tab write admission: migration ownership must block new writers and wait for admitted writers; writers must acquire admission before key resolution and hold it through the write transaction. Applies tosaveImage(),saveBinderAsset(),deleteAllBinderAssetsForProject(),saveStoryCodex(),saveRagVectors(),saveSlice(),createSnapshot().services/storage/protectedStoreMigration.ts#L280).verify()counts current records, butcheckpoint.processedcounts records seen during migration. Cache eviction (services/ai/aiInferenceCacheService.ts) and scene-revision retention (services/sceneRevisionService.ts) can delete records independently of migration state and cause a falseProtectedStoreVerificationShortfallError/recovery-required. Either block those two mutation paths during migration, or verify against the set of records that still exist.Acceptance criteria
beginEncryptionMigration.sceneRevisionAdapterSpec,inferenceCacheAdapterSpec) are included in that production migration run, with checkpoint IDs matching adapterdatabaseName/storeName.CLAUDE.md§ Known Technical Debt B-1 updated once closed.References
services/storage/idbAssetStore.ts#L32-34(cross-tab write admission)services/storage/protectedStoreMigration.ts#L280(verification-vs-deletion race)services/storage/secondaryProtectedStoreAdapters.ts#L136-185(production wiring)docs/PR-310-RECONCILIATION.md— PR feat(storage): encrypt content-bearing secondary databases #310'sPR310-B006/PR310-B007(bulk-disable, passphrase-rotation) designs are explicitly superseded by this migration architecture; that ledger records PR feat(storage): encrypt content-bearing secondary databases #310 remains open until reconciliation rows land, which depends on this Phase-4 work.