Owner: platform team Last reviewed: 2026-05-28
PostgreSQL backups are covered in database-backups.md. This runbook covers the other persistent store: user uploads, generated content, recordings, and project files that live in S3 (production) or MinIO (dev / on-prem).
Object-storage failures are different from Postgres failures:
- The data is much larger (multi-TB to PB).
- A pg_dump-equivalent isn't viable; we rely on the vendor's native versioning + replication.
- Recovery is per-object or per-prefix, not per-database.
What's in scope#
| Bucket / prefix | Domain | What | Recovery priority |
|---|---|---|---|
oshun-prod-uploads/users/<id>/avatars |
yemaya | User avatar images | High |
oshun-prod-uploads/users/<id>/attachments |
yemaya | DM attachments, profile media | High |
oshun-prod-uploads/lilith/personas/<id>/audio |
lilith | Voice clone training audio | Critical (consented BIO) |
oshun-prod-uploads/lilith/voices/<id>/samples |
lilith | Generated voice samples | High |
oshun-prod-uploads/aphrodite/recordings/<stream-id> |
aphrodite | Stream VOD recordings | High |
oshun-prod-uploads/aphrodite/uploads/<id> |
aphrodite | Creator-uploaded clips | High |
oshun-prod-uploads/iris/exports/<request-id> |
iris | GDPR / consent export bundles | Medium (regenerable) |
oshun-prod-uploads/isis/outputs/<job-id> |
isis | Generated images / videos | Medium (regenerable) |
oshun-prod-uploads/veritas/articles/<id>/assets |
veritas | News article media | High |
Buckets NOT in scope (no backups; intentional):
oshun-prod-tmp/*— TTL'd within 24h, no recovery expected.oshun-prod-test/*— test fixtures; recreatable fromlibs/*/testing/fixtures.
Backup strategy#
Production (S3)#
| Layer | Mechanism | Recovery window |
|---|---|---|
| 1. Object versioning | S3 versioning enabled on every in-scope bucket | Indefinite (until lifecycle rule kicks in) |
| 2. Lifecycle transition | Non-current versions → S3 Standard-IA at 30d, Glacier at 90d, expire at retention boundary | Per-bucket retention from pii-inventory.md |
| 3. Cross-region replication | Async replication to oshun-prod-uploads-dr in a second region |
<15 min lag |
| 4. Bucket policy + ACL | Public access blocked; deny non-TLS; require KMS encryption | n/a (preventive) |
Dev (MinIO)#
MinIO doesn't need backups — dev data is recreatable. But to keep parity with the production runbook:
- MinIO versioning is enabled on each bucket
(
mc version enable myminio/<bucket>). - No cross-region replication.
- Lifecycle rules: 7-day expiry on non-current versions.
Recovery procedures#
Single object recovery#
# Find versions of the lost object
aws s3api list-object-versions \
--bucket oshun-prod-uploads \
--prefix "users/$USER_ID/avatars/profile.png" \
--query 'Versions[*].[VersionId,LastModified,IsLatest]' \
--output table
# Copy the version you want back to the current key
aws s3api copy-object \
--bucket oshun-prod-uploads \
--copy-source "oshun-prod-uploads/users/$USER_ID/avatars/profile.png?versionId=$VERSION" \
--key "users/$USER_ID/avatars/profile.png"
Bulk recovery (whole prefix)#
If a prefix was accidentally deleted, restore from versions:
# Get all delete-markers in the prefix
aws s3api list-object-versions \
--bucket oshun-prod-uploads \
--prefix "users/$USER_ID/" \
--query 'DeleteMarkers[?IsLatest==`true`]' > /tmp/markers.json
# Delete the delete-markers, which reveals the previous versions
# as current. (Do not do this without confirming the markers list.)
jq -r '.[] | "\(.Key) \(.VersionId)"' /tmp/markers.json | \
while read key version; do
aws s3api delete-object \
--bucket oshun-prod-uploads \
--key "$key" \
--version-id "$version"
done
Region failover (DR)#
If the primary region is unavailable:
- Verify the DR bucket has the data:bash
aws s3 ls s3://oshun-prod-uploads-dr/<known-prefix>/ --region us-west-2 - Switch reads by toggling the
S3_BUCKETenv var in the affected services (managed via Terraform; do not edit by hand). - Re-establish replication in the reverse direction once the primary region recovers.
The DR cutover doc lives at docs/runbooks/dr-failover.md (TBD).
Validation#
The platform team runs a synthetic backup test on the first Monday of each month:
- Upload a tagged test object to each in-scope bucket.
- Wait 30 minutes for cross-region replication.
- Verify the DR bucket has the object.
- Time the recovery of a small (1 MB) object and a medium (100 MB) object.
- Post the timings to
#platform-ops.
If any step fails, file a SEV-2 ticket immediately.
What's NOT covered here#
- Postgres logical dumps — see database-backups.md.
- Redis snapshots — a dedicated
redis-backup.mdrunbook is TBD (task #107). - Kafka topic backups — n/a; events are reprocessable from source of truth.
- Application code / config — the git repo is the backup; deploys are reproducible from a commit hash.