diff --git a/docs/runbook-migrations.md b/docs/runbook-migrations.md new file mode 100644 index 0000000..9c1dcac --- /dev/null +++ b/docs/runbook-migrations.md @@ -0,0 +1,65 @@ +# nsecbunkerd migration & DB-maintenance runbook + +Operational notes for applying schema migrations and ACL/pairing maintenance on +deployed, **LNbits-connected** nsecbunkerd instances. + +## ⚠️ Never full-wipe `nsecbunker.db` on an LNbits-connected instance + +The nsecbunkerd ↔ LNbits pairing is **split across both systems**: + +- **Bunker** (`nsecbunker.db`): per-account `KeyUser` binding (keyed by LNbits's + stable client pubkey) + redeemed `Token` + a shared `Policy`. +- **LNbits** (`accounts.signer_config`, `RemoteBunkerSigner`): + `{token, client_nsec, policy_id}`. + +`RemoteBunkerSigner.sign_event()` signs **directly with the stored `client_nsec`** +— it does NOT re-connect/re-redeem, and there is **no auto-repair** on restart or +sign-failure. `provision()` runs only at new-account creation and mints a NEW +npub (which changes the user's nostr identity). + +**Consequence of a full `nsecbunker.db` wipe:** the `KeyUser` bindings are +deleted → every LNbits account's stored config dangles → all signing fails, and +the only standard "repair" (`provision()`) changes identities. **Do not do it.** + +### Correct way to strip the #24 materialized photocopies + +Post-#27, token grants are evaluated live via the ACL step-4 `Token → Policy → +PolicyRule` join, so the old materialized `SigningCondition` rows are redundant +— and, written with `expiresAt = NULL`, they would keep granting past a token's +expiry (silently re-opening #24 for already-paired clients). Strip them with a +**targeted delete** that preserves the pairing: + +```sql +-- Verify first. +SELECT COUNT(*) FROM Token WHERE redeemedAt IS NOT NULL; -- bindings to preserve +SELECT COUNT(*) FROM SigningCondition; -- photocopies to strip + +-- Keeps KeyUser + Token + Policy intact. Live-token clients keep working +-- untouched; only the stale photocopies are removed. +DELETE FROM SigningCondition; +``` + +Run against each instance's `nsecbunker.db`. If an instance was already +full-wiped, recover by restoring the pre-wipe `nsecbunker.db` backup, then run +the targeted delete. + +> Manual-override grants (`add_signing_condition`, web-approval) also live in +> `SigningCondition`. On an LNbits-only bunker there typically are none, so a +> blanket `DELETE FROM SigningCondition` is safe. If an instance uses manual +> overrides, delete only the policy-derived rows you intend to strip. + +## Keys are never in the DB + +Key material lives in `nsecbunker.json` (`keys`), never in `nsecbunker.db`. A DB +wipe loses ACL/pairing state, never keys. LNbits holds the bunker **admin nsec** +(`LNBITS_NSEC_BUNKER_ADMIN_NSEC`) and is the sole admin client. + +## Schema migrations + +Migrations are applied by the deploy's `prisma migrate deploy`, **not** by the +daemon on boot — the in-`start.js` `npm run prisma:migrate` step is a no-op +(tracked in #31). After adding a migration, make sure the deploy applies it. + +Prisma on NixOS needs the engine env pinned to `prisma-engines_6` (the bare +`prisma-engines` attr is now 7.x with no `libquery_engine.node`; devShell fix +tracked in #30). The deploy's `package.nix` already pins `_6`.