Backups
Carabase ships with a self-contained backup pipeline that covers both the Postgres database and the binary blob bytes that live on disk (photos, voice memos, import zips). It’s the first thing you should set up before putting anything real into production. Both surfaces share the same encryption envelope, the same HOST_MASTER_KEY, and the same retention math, so a single nightly cron job covers everything.
Layout
Section titled “Layout”$CARABASE_BACKUP_DIR/ # default: ~/.carabase/backups├── dev/ # ← DB backups│ └── 2026-04-09/│ └── 2026-04-09T031700.sql.zst.enc├── staging/├── prod/│ ├── 2026-04-09/│ │ └── 2026-04-09T031700.sql.zst.enc│ └── cron.log└── blobs/ # ← blob backups ├── dev/ │ └── 2026-04-09/ │ └── 2026-04-09T031700.tar.gz.enc ├── staging/ └── prod/ └── 2026-04-09/ └── 2026-04-09T031700.tar.gz.encEvery file is encrypted with HOST_MASTER_KEY using the same AES-256-GCM envelope the host uses for credential encryption (version(1) + iv(12) + ciphertext + authTag(16)).
- Database backups are a single streamed
pg_dump -Fp | zstd -T0 | encryptpipeline →.sql.zst.enc. Nothing is ever staged as a plaintext dump on disk, so backing up a large database no longer needs roughly twice its size in free space.zstd -T0uses all cores, which is far faster than single-coregzipon vector-heavy dumps. Whenzstdisn’t installed, the pipeline falls back to streamedgzip -9 → .sql.gz.enc— still no temp dump, just slower. - Blob backups are
tar | gzip | encryptover each workspace’sartifacts/andimports/directories →.tar.gz.enc.
Restore is format-transparent: the restore script picks the decompressor from the file extension, so legacy .sql.gz.enc backups still restore.
Photo / attachment / import bytes do NOT ride inside pg_dump. They live under $CARABASE_DATA_DIR/workspaces/*/ (default ~/.carabase/data/workspaces/*/). Without the companion blob backup, a database-only restore would leave every file_artifacts row pointing at a path that no longer exists — so back up both.
Scripts
Section titled “Scripts”| Script | What it does |
|---|---|
pnpm backup:<env> |
Streamed pg_dump | zstd -T0 | encrypt → atomic .sql.zst.enc, then auto-prunes. Refuses to run without HOST_MASTER_KEY. Compression level via BACKUP_ZSTD_LEVEL (default 10); skip the auto-prune with BACKUP_SKIP_PRUNE=1. |
pnpm backup:blobs <env> |
tar | gzip | encrypt over artifacts/ + imports/. No plaintext archive ever touches disk. Refuses without HOST_MASTER_KEY; writes an empty archive when the data dir is missing. |
pnpm backup:prune <env> / backup:blobs:prune <env> |
Retention: keep the newest backup per day for the last few days, plus the newest from each of the prior couple of weeks and the prior month — capping total disk use. The DB backup runs this automatically after each run. --dry-run to inspect. |
pnpm backup:status |
Reports the age of the newest DB and blob backup per env. Ladder: PASS < 30h, WARN 30h–72h, FAIL ≥ 72h. The blob check is skipped silently when the data dir is tiny. Exits non-zero if any env is FAIL. |
pnpm backup:restore <env> <file> |
Decrypts into a chmod-600 tempfile, prompts for restore confirmation (or pass --yes), applies via psql --single-transaction --set ON_ERROR_STOP=on. Refuses to restore into prod unless --i-know is passed. |
pnpm backup:blobs:restore <env> <file> |
Decrypts to a tempfile on the same filesystem as $CARABASE_DATA_DIR, verifies the AES-GCM auth tag, then extracts via tar. Decrypt-then-extract is deliberate: streaming straight into tar could partially overwrite the target before a bad-tag error fires. Same prod guardrail. |
pnpm backup:install-cron <env> |
Writes a launchd plist that runs the DB backup, blob backup, and both prunes every night at 03:17 local. Pass --uninstall to remove it. |
Both backups run before either prune, so a retention-step failure can’t gate the next day’s bytes. A DB-backup failure short-circuits everything downstream — the database is the anchor of correctness.
Log rotation
Section titled “Log rotation”The host runs under launchd, which redirects stdout/stderr to ~/.carabase/logs/<env>.log and never rotates it — left alone, that log can grow until it fills the disk. Two helpers keep it bounded:
| Script | What it does |
|---|---|
pnpm logs:cap [--dry-run] |
Truncates each log in place (preserving a recent tail) once it grows past LOG_CAP_MAX_BYTES (default 500 MB). Truncate-in-place is the only mechanism that reclaims space while the host holds the log file open. |
pnpm logs:install-cron |
Loads a launchd job that runs the cap hourly. |
Daily workflow
Section titled “Daily workflow”# One-time: install the cron for prod on the always-on hostpnpm backup:install-cron prod
# Any time, check healthpnpm backup:status# → [dev ] PASS latest=2026-04-09T03:17:12Z age=7.2h ago# [staging] PASS latest=2026-04-09T03:17:14Z age=7.2h ago# [prod ] PASS latest=2026-04-09T03:17:18Z age=7.2h ago# blobs PASS latest=2026-04-09T03:17:35Z age=7.2h ago data=12.4 GiB
# Restore a prod dump into dev for debugging — DB first, then blobspnpm backup:restore dev ~/.carabase/backups/prod/2026-04-09/2026-04-09T031700.sql.zst.encpnpm backup:blobs:restore dev ~/.carabase/backups/blobs/prod/2026-04-09/2026-04-09T031700.tar.gz.encRestore the database first so the file_artifacts rows exist before their bytes arrive on disk.
Production guardrail
Section titled “Production guardrail”backup:restore and backup:blobs:restore refuse to write into prod unless the env arg is prod and --i-know is passed. The restore also refuses if DATABASE_URL looks like production but the env arg says otherwise. This is the same friction pattern as db:reset — the explicit flag is what proves you mean it.
Disk capacity & offsite storage
Section titled “Disk capacity & offsite storage”Keeping backups on the same internal disk as a large working set isn’t sustainable as the mesh grows — a full disk blocks the nightly DB backup, which in turn short-circuits the chained blob backup. The durable fix is offsite or external storage: point CARABASE_BACKUP_DIR at an external volume or remote mount so the DB and blob backups (and their retention) live off the working disk. Until then, backup:<env> has a pre-flight free-space guard that aborts cleanly (rather than failing mid-stream) when free space is below what the compressed dump needs; override the threshold with BACKUP_MIN_FREE_MB.
Restoring on a different machine
Section titled “Restoring on a different machine”A backup is just a .sql.zst.enc (or legacy .sql.gz.enc) and a .tar.gz.enc file — portable. Copy both archives plus the same HOST_MASTER_KEY to the target machine, then run pnpm backup:restore <env> <db-file> followed by pnpm backup:blobs:restore <env> <blobs-file>. Without the master key, the files are worthless — never store the key alongside the backups, and never lose it.
On the roadmap
Section titled “On the roadmap”- Off-machine cloud sink (e.g. Backblaze B2 / iCloud Drive).
/api/v1/healthdetail that surfacesbackup:status, so the desktop can show a “backup stale” warning.- PITR / WAL archiving for the prod Postgres instance.