Appearance
Backups
What needs backing up, and why
| data | can you rebuild it? |
|---|---|
records/ | yes: immutable, write-once files. Copy them anywhere and sync them back |
server-key.json | no: if it changes, every existing record stops verifying against the published key |
gammonchain.db | no |
Losing the database locks out every registered player permanently
gammonchain.db holds registered players' keys, their email addresses, ratings, and live sign-in tokens. For an account, the stored key is the identity. Lose the file and every registered player is locked out for good. They cannot re-register, because the key is gone.
src/server/backup.js takes care of the database. The server key and records are files you keep on a persistent volume. Back them up with the volume, or keep the key as a secret (SERVER_KEY_PKCS8).
Configuration
Backups are off unless all four of endpoint, bucket, key and secret are set:
bash
BACKUP_S3_ENDPOINT=https://<account>.r2.cloudflarestorage.com
BACKUP_S3_BUCKET=gammonchain-backups
BACKUP_S3_KEY=...
BACKUP_S3_SECRET=...
BACKUP_S3_REGION=auto # R2 wants "auto"; B2/AWS want a real region
BACKUP_INTERVAL_H=6 # hours between snapshots (minimum 1)
BACKUP_PREFIX=gammonchain # object key prefixDEPLOY.md suggests Cloudflare R2 (10 GB free, no egress fees) or Backblaze B2 (10 GB free). Both speak the S3 API. Changing provider means changing the endpoint and credentials.
With backups unset, nothing runs: no timer and no warning on every boot. /healthz reports backup.configured: false, so the state is visible instead of assumed.
With deploy/docker-compose.prod.yml, all of these (including BACKUP_INTERVAL_H and BACKUP_PREFIX) are read from deploy/.env.
How it works
A consistent snapshot: VACUUM INTO
Copying a .db file while the server is running is the classic way to get a backup that restores into a corrupt database. SQLite may be mid-transaction, and the -wal file holds pages the main file does not have yet. Instead, the backup runs
sql
VACUUM INTO '/tmp/gc-backup-<pid>-<run>.db'This asks SQLite itself for a clean, complete copy at a single point in time. Under WAL mode it does not block writers. The temporary file is deleted afterwards whether the upload succeeds or not.
Upload: hand-written SigV4
The snapshot is uploaded with a single PUT signed with AWS Signature Version 4. signV4() is about sixty lines built on node:crypto, which the code comment considers a much smaller liability than an SDK dependency tree. The details that are easy to get wrong are all explicit:
- the payload hash is computed over the actual body (S3 verifies it);
- headers are signed in lower-case, sorted order:
content-type,host,x-amz-content-sha256,x-amz-date; - the credential scope date matches
x-amz-dateto the day; - the object path uses path-style addressing:
<endpoint>/<bucket>/<key>.
Objects are named <prefix>/<ISO timestamp with : and . replaced by ->.db, so the bucket lists in date order and a lifecycle rule can expire old snapshots by prefix. The server never deletes old backups. Set up a lifecycle rule in the bucket if you want retention.
Schedule and failure reporting
- One backup runs immediately at startup, so a misconfiguration shows up at deploy time, not six hours later. After that, one runs every
BACKUP_INTERVAL_Hhours. - A failed backup never throws. It records
lastError, sends the error to the error reporter (kind: backup), and logs[backup] FAILED. /healthz→backupshows{ configured, intervalH, runs, lastOk, lastError }.
After deploying
Check that /healthz shows backup.configured: true, and a few minutes later that backup.lastOk is set. A backup that silently fails is exactly the kind of thing nobody notices until the day it matters.
Restoring
The repository has no restore script. A snapshot is an ordinary SQLite database file. To restore, stop the server, put the downloaded snapshot at <DB_DIR>/gammonchain.db (with no stale -wal/-shm files next to it), and start the server again.