Skip to content

chore(ops): weekly off-Neon pg_dump backup script (#58)#84

Merged
Sootopolis merged 1 commit into
mainfrom
wip
Jun 13, 2026
Merged

chore(ops): weekly off-Neon pg_dump backup script (#58)#84
Sootopolis merged 1 commit into
mainfrom
wip

Conversation

@Sootopolis

Copy link
Copy Markdown
Owner

What

Adds scripts/backup-neon.sh — a weekly compressed logical dump of the CCAS database to local disk, plus a README Backups section and .env.example entries.

Why

Neon's free plan retains only 24h of PITR. A bad write not caught within a day is unrecoverable. This is the missing disaster-recovery floor. Pure ops — no Scala touched.

Design

  • Connection reuse: DATABASE_URL (JDBC form, jdbc: prefix stripped to a libpq URI) takes priority, else DB_*. The password is routed to PGPASSWORD (with urldecode) so it never lands in argv (no leak via ps / /proc/<pid>/cmdline).
  • Schema-only excludes (--exclude-table-data) for the rebuildable api_response_cache / api_response_body / api_fetch_failure tables — restore recreates empty tables, app refills them.
  • Atomic: dumps to $OUT.tmp, mvs to the final name only on success → a failed/partial dump never masquerades as valid or evicts a good backup via the retention prune.
  • Retention prune keeps newest CCAS_BACKUP_RETAIN (default 6).

Verification

  • Isolation tests: password extraction (5 cases incl. percent-encoded), urldecode, atomic temp+mv (fail → no artifacts; success → promote), retention prune.
  • Smoke-tested against prod Neon: 9.3M dump in ~10s, valid archive (pg_restore --list, 202 entries), exclusions confirmed (3 excluded tables keep DDL, drop data; 29 core tables retain rows).
  • Pre-push sbt test: 791 passed, 0 failed.

Closes #58.

🤖 Generated with Claude Code

Neon's free plan retains only 24h of PITR, so a bad write not caught
within a day is unrecoverable. Add scripts/backup-neon.sh: a weekly
compressed logical dump to local disk as the disaster-recovery floor.

- Reuses the app's connection config: DATABASE_URL (JDBC, jdbc: prefix
  stripped to a libpq URI) or DB_* fallback. Password is routed to
  PGPASSWORD (never argv) so it can't leak via ps / /proc/<pid>/cmdline.
- Dumps schema-only (--exclude-table-data) for the rebuildable
  api_response_cache / api_response_body / api_fetch_failure tables to
  keep dumps small (~9MB vs cache bloat).
- Atomic: writes to $OUT.tmp and mv's to the final name only on success,
  so a failed/partial dump never masquerades as valid or evicts a good
  backup via the retention prune.
- Retention prune keeps the newest CCAS_BACKUP_RETAIN (default 6) dumps.

README gains a Backups section (cron line, restore command + pg_restore
version note, env knobs); .env.example documents the two backup vars.

Smoke-tested against prod Neon: 9.3M dump, valid archive, exclusions
verified (excluded tables keep DDL, drop data; 29 core tables retain rows).

Closes #58.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Sootopolis Sootopolis merged commit 2a284ce into main Jun 13, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Weekly off-Neon pg_dump backup (24h PITR is not enough)

1 participant