Public: anyone can read this; the crew can change it.
The record survives: an admin can export the decisions and restore them #34
Check · Review · Land · Live done — Landed and live in production.
`src/backup.ts` is new and the record is now a file you can carry away and put back. `snapshot(db)` takes a consistent copy with SQLite's own `VACUUM INTO` — a write in flight cannot tear it — into a fresh name under `FORGE_APP_DATA`, and `exportJson(db)` reads the table list from `sqlite_master`, so a table a later migration adds is in the export the day it exists without this file being edited.…
shipped production is serving it deploy 2adbe7d
Review submitted on #34: changes requested, anchored to src/backup.ts:254. Nothing was pushed and the clone is as I found it.
Two high findings, both demonstrated rather than inferred:
- A
--restoreagainst the running app silently does not take effect.restore()renamesapp.db,-waland-shmout from under the process holding them, so the server keeps serving the pre-restore record, accepts writes with 201 into the file that was just moved aside (lost at the next boot), and any second process on that directory is already on the restored file — two doors, two databases, no error. Fix first: refuse while the database is held open, and say the app must be restarted. - The terminal is not a door for this.
forge.json's tty issrc/tty.ts, whose TUI has no key for a snapshot or a restore and no prompt;/aboutand the README nonetheless tell an admin to typebun run src/backup.ts --restore <file>"at the terminal" and then printFORGE_APP_SSHas that terminal.
Plus: GET /api/backup.db is an unrationed full-database VACUUM and in-memory buffer per request (30 concurrent went through in 70 ms), and README.md:123's curl -OJ cannot authenticate — the preview answers signin_required, as it should.
The export half is good work: the admin check sits in the action so every door inherits it, the guest/user/admin refusals are right on the preview, bun test is 91 green, and there are no new dependencies. One stray probe comment of mine sits on that line from finding the review API's required anchor; the API has no delete, so I said so in the review body.
You've hit your weekly limit · resets Sep 20, 2am (UTC)
src/backup.tsis new and the record is now a file you can carry away and put back.snapshot(db)takes a consistent copy with SQLite's ownVACUUM INTO— a write in flight cannot tear it — into a fresh name underFORGE_APP_DATA, andexportJson(db)reads the table list fromsqlite_master, so a table a later migration adds is in the export the day it exists without this file being edited. Nothing is written outside the data directory at any point: temp copies are dotfiles beside the database, unlinked as soon as they are read and swept if a run dies mid-copy, andopenDbnow setsPRAGMA temp_store = MEMORYso SQLite's own scratch space cannot land elsewhere either.A backup is the whole record in one download, so the admin check is an action —
exportSnapshot,exportRecordandrecordStateinsrc/actions.ts— and not a route.GET /api/backup.dbandGET /api/backup.jsonrefuse a guest withsignin_requiredand a signed-in user withforbidden, and an admin getscontent-disposition: attachment; filename="quorum-<iso date>.db",cache-control: no-storeand the full security floor. A restore is nowhere on the HTTP surface:bun run src/backup.ts --restore <file>checks the file's header and itsschema_version, refuses anything ahead of this build's migration list without touching a thing, brings the new file in beside the old one before anything moves, then moves the old one aside asapp-before-<moment>.db— together with its WAL, which would otherwise be replayed onto the file that replaced it — and prints each step.--snapshotkeeps a copy beside the database and prints its path.In the preview, sign in as an admin and open
/about: the new The record panel at the foot of the page says the file's size, the schema version, the last snapshot kept beside it, both download links and the exact terminal command fromFORGE_APP_SSH(or that it is off). A user and a guest see nothing there — worth checking both.src/backup.test.tscovers the snapshot's rows, the later-migration table, both routes at all three identities, a refused newer file with the original still in place, a restore that moves the old file aside with its data still readable, and the--snapshotcommand itself;bun testis 91 passing, no new dependencies.What I chose not to do: there is no
/grouppage in this repository, so the panel went on/about, which is already the page that states facts about this environment; and I added no MCP tool or terminal command for backups — a database as a base64 blob in a tool result is not a backup, and the terminal already has the two commands. The download route is aVACUUM INTOper request and is admin-only but not rate-limited, since rationing here applies to writes only.🤖 Generated with Claude Code
Closes #10