Backups & DR
Back up Postgres, Qdrant, and storage – and restore safely.
A Fosnie backup captures everything stateful: the database (application data and the audit log), the Qdrant vectors, and the storage directories. Redis is ephemeral and isn't backed up.
| Store | Contents | Method |
|---|---|---|
| Postgres | app data + audit hash-chain | pg_dump -Fc |
| Qdrant | every collection's vectors | snapshot API |
| Storage | documents, workspace, artefacts, exports, branding, skills, prompts | tar -z |
A backup is a timestamped bundle (postgres.dump, per-collection Qdrant snapshots, storage.tar.gz, a
manifest, and checksums). Set an age recipient to encrypt the whole bundle.
Schedule backups
Configure /etc/pai/backup.env:
PAI_DB_URL=postgres://pai:PASS@127.0.0.1:5432/pai
PAI_QDRANT_URL=http://127.0.0.1:6333
PAI_STORAGE_DIRS="/var/lib/pai/documents /var/lib/pai/workspace /var/lib/pai/artefacts /var/lib/pai/exports /var/lib/pai/branding /var/lib/pai/skills /var/lib/pai/prompts"
PAI_BACKUP_DIR=/var/backups/pai
PAI_BACKUP_RETAIN=14
# PAI_BACKUP_AGE_RECIPIENT=age1... # optional encryptionInstall the scripts and enable the timer:
install -m0755 deploy/scripts/backup.sh deploy/scripts/restore.sh /opt/pai/deploy/scripts/
cp deploy/systemd/pai-backup.{service,timer} /etc/systemd/system/
systemctl enable --now pai-backup.timer
systemctl start pai-backup.service # take one now; check with journalctl -u pai-backupThe timer runs daily by default – tighten it to hourly if your tolerance for data loss is smaller. Your recovery-point objective equals the backup interval; sub-hour or zero-loss recovery needs Postgres point-in-time recovery (WAL archiving), which is a separate tier.
Restore
Restore is ordered – Postgres → Qdrant → storage – and the script verifies checksums first and
refuses to overwrite a populated database without --force:
# decrypt first if the bundle is encrypted:
# age -d -i key.txt bundle.tar.age | tar -x
PAI_DB_URL=postgres://pai:PASS@HOST:5432/pai \
PAI_QDRANT_URL=http://HOST:6333 \
deploy/scripts/restore.sh /var/backups/pai/pai-backup-<timestamp>Verify the audit chain before cutover. Start the backend on the restored database and export the
audit log (Admin → Audit, or GET /api/admin/audit/export). A bad result means the chain didn't
survive – restore an earlier known-good bundle rather than going live. Then smoke-test chat, a document,
and retrieval. Test-restore into a scratch environment quarterly.