Relationships
#650 datastore setup migration relocates and deletes repo-root .swamp/secrets, breaking all local_encryption vault.get
Opened by keeb · 6/13/2026· Shipped 6/15/2026
Description
Running swamp datastore setup extension <type> --config ... as a full migration (i.e. without --skip-migration) relocates the repo-root <repo>/.swamp/secrets directory into the datastore cache and then deletes the repo-root copy. But the local_encryption vault always reads from <base_dir>/.swamp/secrets/local_encryption/<vault> (base_dir = repo root), independent of the datastore. After setup, every ${{ vault.get(...) }} fails and swamp vault list-keys / read-secret report empty / "not found". The repo is functionally broken for any vault-dependent operation until the secrets are manually restored.
This is datastore-backend-agnostic: it reproduces for any extension datastore (and the filesystem datastore), because the migration target list is core's DEFAULT_DATASTORE_SUBDIRS, which includes secrets.
There is also a security dimension when the target datastore syncs the tier to shared storage: the local_encryption .key (symmetric key) sits right next to its .enc ciphertext, so migrating/syncing secrets/ co-locates key + ciphertext in the shared backend (encryption-at-rest defeated). This report is about the functional break; the sync-exposure side is being fixed independently in the affected extension.
Root cause
datastoreSetupExtensionhardcodes the migration config withoutdirectories:src/libswamp/datastores/setup.ts:376→const config = { type: "filesystem", path: cachePath };migrateDatastoreresolves the dirs to copy viagetDatastoreDirectories(config), which falls back toDEFAULT_DATASTORE_SUBDIRSwhendirectoriesis absent:src/domain/datastore/datastore_migration_service.ts:57andsrc/domain/datastore/datastore_config.ts:171.DEFAULT_DATASTORE_SUBDIRSincludes"secrets":src/domain/datastore/datastore_config.ts:49.- On success, setup deletes every migrated source dir:
src/libswamp/datastores/setup.ts:463(cleanupSourceDirs(<repo>/.swamp, directoriesMigrated)), whichDeno.remove(..., {recursive:true})s<repo>/.swamp/secrets. - The vault reads from a fixed repo-root path, unaffected by the datastore cache:
src/domain/vaults/local_encryption_vault_provider.ts:80-88({base_dir}/.swamp/secrets/local_encryption/{vault}).
Net effect: the migration moves the vault's storage out from under it and deletes the original.
Steps to reproduce
- Repo with a
local_encryptionvault holding a secret under<repo>/.swamp/secrets/local_encryption/<vault>/. swamp datastore setup extension <type> --config ...(no--skip-migration).swamp vault list-keys <vault>→count: 0; a model's${{ vault.get(...) }}no longer resolves.<repo>/.swamp/secretsis gone; the files now live only under the datastore cache.
Proof (drives the real core functions)
Reproduced end-to-end against the core source by invoking the exact functions the setup flow uses — migrateDatastore (the setup.ts:376 call), the mirrored cleanupSourceDirs deletion, and the real LocalEncryptionVaultProvider:
[1] Secret written. Repo-root vault dir contents:
- ROUTER_ADMIN_PASSWORD.enc
- .key
[1] vault.get before migration => "hunter2-super-secret"
[2] DEFAULT_DATASTORE_SUBDIRS includes 'secrets': true
[2] getDatastoreDirectories(...) includes 'secrets': true
[3] migrateDatastore result.directoriesMigrated: [ "data", "secrets" ]
[3] 'secrets' was migrated into the cache: true
[3] cache now holds the secret .enc: .key, ROUTER_ADMIN_PASSWORD.enc
[4] repo-root .swamp/secrets deleted by cleanup: true
[5] vault.get after migration FAILED: true
error: Secret 'ROUTER_ADMIN_PASSWORD' not found in local vault 'infra'
==== ISSUE #6 REPRODUCED ✅ ====Workaround
Restore the repo-root secrets from a pre-migration copy (it lives outside the cache, so it won't re-sync):
cp -a <backup>/.swamp/secrets <repo>/.swamp/secretsSuggested fix
The secrets/ tier should never be part of the datastore migration. Options, narrowest first:
- Exclude
secretsfrom the migration indatastoreSetupExtension/ the filesystem setup path (don't copy or delete the vault tier). - Remove
"secrets"fromDEFAULT_DATASTORE_SUBDIRSso no backend ever migrates/syncs it. - Alternatively, have
local_encryptionread from the datastore cache path when an extension datastore is active (more invasive; couples the vault to the datastore).
Whichever path is taken, the migration must not delete <repo>/.swamp/secrets while the vault still reads it.
Environment
- swamp CLI:
20260206.200442.0(root cause verified against currentmain, commitdf3947ba) - Originally observed with
@keeb/mongodb-datastore2026.05.05.1 on swamp20260513.132340.0, MongoDB 7.x replica set - Backend-agnostic — the migration list is core's
DEFAULT_DATASTORE_SUBDIRS
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.