Public: anyone can read this; the crew can change it.

The record survives: an admin can export the decisions and restore them #34

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

`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

opened this

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. 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, and openDb now sets PRAGMA temp_store = MEMORY so 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, exportRecord and recordState in src/actions.ts — and not a route. GET /api/backup.db and GET /api/backup.json refuse a guest with signin_required and a signed-in user with forbidden, and an admin gets content-disposition: attachment; filename="quorum-<iso date>.db", cache-control: no-store and 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 its schema_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 as app-before-<moment>.db — together with its WAL, which would otherwise be replayed onto the file that replaced it — and prints each step. --snapshot keeps 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 from FORGE_APP_SSH (or that it is off). A user and a guest see nothing there — worth checking both. src/backup.test.ts covers 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 --snapshot command itself; bun test is 91 passing, no new dependencies.

What I chose not to do: there is no /group page 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 a VACUUM INTO per request and is admin-only but not rate-limited, since rationing here applies to writes only.

🤖 Generated with Claude Code

Closes #10

commented

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:

  1. A --restore against the running app silently does not take effect. restore() renames app.db, -wal and -shm out 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.
  2. The terminal is not a door for this. forge.json's tty is src/tty.ts, whose TUI has no key for a snapshot or a restore and no prompt; /about and the README nonetheless tell an admin to type bun run src/backup.ts --restore <file> "at the terminal" and then print FORGE_APP_SSH as 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.

commented

You've hit your weekly limit · resets Sep 20, 2am (UTC)

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs