Make the boards survive their file: check the database at boot, and keep a few rolling snapshots beside it.
What it is like today. Every board, column, card and undo row in this app lives in exactly one file — $FORGE_APP_DATA/app.db — and nothing in the repository reads that file for health or ever copies it (grep for integrity_check, VACUUM, backup across src/ and the README finds nothing). A truncated WAL, a full disk during a write, or a bad shutdown takes every board with it, silently: the app would start, CREATE TABLE IF NOT EXISTS would succeed on a damaged file, and the first person to open their board would find it empty with no error anywhere. The README documents limits, rate limits and what is simulated — it says nothing about durability, so nobody using this knows what they are trusting.
Done looks like
[ ] src/backup.ts, one concept: snapshots of the database file. Nothing else goes in it.
[ ] At boot, one PRAGMA quick_check on the open database, logged as a single line — ok, or the first fault it reports. A damaged file is reported loudly at start, not discovered by a person whose board is empty. The app still starts (refusing to start would take a preview down for a fault an operator may want to inspect); the pull states that choice.
[ ] A snapshot is taken with VACUUM INTO '<dir>/snapshots/app-<ISO timestamp>.db', on an interval (default hourly, a named constant), never inside a request — scheduled with an unref'd timer exactly like startWatching in src/watch.ts.
[ ] A snapshot is skipped entirely when nothing has changed since the last one (the revision table already gives a cheap global answer), so an idle app writes no files.
[ ] Retention is bounded and stated: the newest N snapshots are kept (default 5), older ones deleted in the same pass. Every path stays under FORGE_APP_DATA — nothing is written anywhere else, ever.
[ ] A failed snapshot (no space, no permission) is one logged line and never an exception that reaches a request or kills the process.
[ ] Tests that fail without the change, against a temp FORGE_APP_DATA: a snapshot appears and opens as a valid database with the same boards in it; an unchanged database produces no second snapshot; the sixth snapshot removes the oldest; quick_check reports a fault on a deliberately corrupted file.
[ ] A README section says what is snapshotted, how often, how many are kept, where they are, and — plainly — that restoring one is a person copying a file, not a button in the app.
Files I expect to change
src/backup.ts (new), src/server.ts (start it, one line, after the server is listening), src/backup.test.ts (new), README.md.
Risks
Disk: N snapshots of a board database are small, but the cap is what makes that true — make the count and the interval named constants in src/limits.ts alongside every other bound, not literals in the module.
VACUUM INTO holds a read lock for the duration. On this app's data it is milliseconds; say so in a comment and keep the interval long enough that it is never a factor for a writer.
Do not add a restore path, an endpoint, or a download. This issue is about not losing the data; anything that serves a snapshot over HTTP is a new way to leak every board and belongs in its own issue with its own review.
Make the boards survive their file: check the database at boot, and keep a few rolling snapshots beside it.
What it is like today. Every board, column, card and undo row in this app lives in exactly one file —
$FORGE_APP_DATA/app.db— and nothing in the repository reads that file for health or ever copies it (grepforintegrity_check,VACUUM,backupacrosssrc/and the README finds nothing). A truncated WAL, a full disk during a write, or a bad shutdown takes every board with it, silently: the app would start,CREATE TABLE IF NOT EXISTSwould succeed on a damaged file, and the first person to open their board would find it empty with no error anywhere. The README documents limits, rate limits and what is simulated — it says nothing about durability, so nobody using this knows what they are trusting.Done looks like
src/backup.ts, one concept: snapshots of the database file. Nothing else goes in it.PRAGMA quick_checkon the open database, logged as a single line —ok, or the first fault it reports. A damaged file is reported loudly at start, not discovered by a person whose board is empty. The app still starts (refusing to start would take a preview down for a fault an operator may want to inspect); the pull states that choice.VACUUM INTO '<dir>/snapshots/app-<ISO timestamp>.db', on an interval (default hourly, a named constant), never inside a request — scheduled with an unref'd timer exactly likestartWatchinginsrc/watch.ts.revisiontable already gives a cheap global answer), so an idle app writes no files.FORGE_APP_DATA— nothing is written anywhere else, ever.FORGE_APP_DATA: a snapshot appears and opens as a valid database with the same boards in it; an unchanged database produces no second snapshot; the sixth snapshot removes the oldest;quick_checkreports a fault on a deliberately corrupted file.Files I expect to change
src/backup.ts(new),src/server.ts(start it, one line, after the server is listening),src/backup.test.ts(new),README.md.Risks
src/limits.tsalongside every other bound, not literals in the module.VACUUM INTOholds a read lock for the duration. On this app's data it is milliseconds; say so in a comment and keep the interval long enough that it is never a factor for a writer.Suggested: mason, reviewed by sentinel.