Files
vyndr/docs/BACKUP-RUNBOOK.md
T
builtbykev 5f5c004416 Item 2 — nightly pg_dump backup script + runbook (Supabase free tier has none)
scripts/backup-db.sh: nightly full-DB pg_dump via the direct connection string,
14-day local rotation, weekly off-box rsync copy, ntfy alert on any failure +
an undersized-dump guard (an empty dump is a silent failure). docs/BACKUP-
RUNBOOK.md: the ONE env var Kev must set (SUPABASE_DB_URL — the direct
db.<ref>.supabase.co:5432 URI, not the pooler), the cron line, the off-box
target (Hetzner Storage Box via rsync, simplest for a Hetzner box), and the
restore FINGERPRINT procedure (pg_restore into a scratch DB + count
ledger_entries — proves it's a real, restorable backup).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 13:55:39 -04:00

2.8 KiB

VYNDR Backup Runbook (security follow-up item 2)

Supabase free tier has zero backups (no scheduled, no PITR). scripts/backup-db.sh is the safety net: a nightly full-database pg_dump, 14 days kept locally, a weekly copy pushed off-box, ntfy alert on any failure.

What Kev needs to set (one env var)

SUPABASE_DB_URL — the Supabase direct connection string (session mode). Supabase → Project → Settings → Database → Connection string → URI, the db.<ref>.supabase.co:5432 one (NOT the :6543 transaction pooler — pg_dump needs a real session). Paste it in Coolify as an env var; never commit it.

Optional: BACKUP_REMOTE (off-box rsync target for the weekly copy — see below).

Install the cron (on the Hetzner box)

# 1. Ensure the client is present
sudo apt-get install -y postgresql-client rsync

# 2. Nightly at 03:10 UTC. Pass the env the script needs (or source an env file).
sudo crontab -e
# add:
10 3 * * * SUPABASE_DB_URL='postgresql://...' BACKUP_REMOTE='u123456@u123456.your-storagebox.de:vyndr-backups/' /path/to/vyndr/scripts/backup-db.sh >> /var/log/vyndr-backup.log 2>&1

(If the cron runs inside the Coolify container instead, the env vars are already present — just schedule scripts/backup-db.sh.)

Off-box target (simplest reliable pick)

Hetzner Storage Box over rsync/SSH — you're already on Hetzner, it's ~€3/mo for 1TB, and needs no extra tooling. Create one, add the box's SSH key to it, set BACKUP_REMOTE=u<id>@u<id>.your-storagebox.de:vyndr-backups/. The script pushes the latest dump every Sunday. (Alternative: Backblaze B2 via rclone if you'd rather keep it off Hetzner entirely — swap the rsync line for rclone copy.)

Restore / FINGERPRINT (proves it's a real backup, not just a file)

A dump only counts once a restore of it succeeds. Load one into a scratch DB:

# spin a throwaway local postgres, restore the newest dump, count a known table
docker run -d --name vyndr-restore-test -e POSTGRES_PASSWORD=x -p 55432:5432 postgres:15
sleep 5
newest=$(ls -t /var/backups/vyndr/vyndr-*.dump | head -1)
pg_restore --no-owner --no-privileges -d "postgresql://postgres:x@localhost:55432/postgres" "$newest"
psql "postgresql://postgres:x@localhost:55432/postgres" -c "select count(*) from public.ledger_entries;"
docker rm -f vyndr-restore-test

A non-zero ledger_entries count from the restored dump = the backup is real and restorable. Record the date + row count as the fingerprint.

Alerting

Any hard failure (missing env, pg_dump error, empty/undersized dump) pages ntfy topic vyndr-backups-kev2026 at urgent priority. The weekly off-box push failing (or BACKUP_REMOTE unset) pages at high priority but does not fail the run — the local dump still succeeded. Subscribe the phone to that topic.