Vendale engineering
Backup and recovery
Snapshot schedules, off-machine copies, and the restore-validation procedure.
Maintained with the code · Download .md
Current Status
Production backup infrastructure has not been activated. This document specifies the required backup scope, retention policy, recovery procedure, and encryption-key dependency for Vendale. All steps below are pre-activation requirements; none have been executed in a live environment.
---
1. Backup Scope
| Component | Backup method | Minimum frequency |
|---|---|---|
| PostgreSQL — full database | pg_dump or provider snapshot | Daily |
| PostgreSQL — WAL segments | WAL archiving or continuous backup | Continuous (point-in-time recovery) |
| Object storage — media bytes | Provider bucket versioning or cross-region replication | Continuous or daily sync |
| Backend environment configuration | Secrets manager export (exclude plaintext secrets) | On every configuration change |
| Alembic migration head | Recorded in release notes and this document | On every release |
WHATSAPP_CREDENTIAL_ENCRYPTION_KEY | Secrets manager and off-site recovery key store | Immutable; never rotated without re-encryption plan |
Items not required to be backed up separately (recoverable from code):
- Application source code (version controlled in git).
- Database schema (recovered via
alembic upgrade headagainst a blank database). - Tauri installer artifacts (rebuilt from tagged commit).
---
2. Encryption Key Dependency
WHATSAPP_CREDENTIAL_ENCRYPTION_KEY is a 64-character hex AES-256 key stored in the backend environment only. It encrypts all WhatsApp access tokens in the whatsapp_connections table using AES-256-GCM.
If this key is lost or rotated without re-encryption, all stored WhatsApp access tokens become permanently unreadable. Operators must re-enter access tokens for every WhatsApp connection.
Required storage policy:
- Store the key in the primary secrets manager.
- Store a recovery copy in an offline or isolated secondary location (e.g., hardware security module, printed sealed copy in a physical safe).
- Never store the key in version control, application logs, or database rows.
- Never pass the key as a command-line argument.
- Rotation procedure: decrypt all connection tokens with the old key, re-encrypt with the new key, update the env var, restart the backend. This requires a brief maintenance window and must be tested in a staging environment first.
---
3. PostgreSQL Backup Procedure
3.1 Full Dump (Manual)
export PGPASSWORD=<production-password> pg_dump \ --host=<production-host> \ --port=5432 \ --username=<production-user> \ --format=custom \ --compress=9 \ --file="vendale_$(date +%Y%m%d_%H%M%S).pgdump" \ vendale
Store the dump in the object storage backup bucket or off-site storage. Verify the dump is readable before declaring backup complete:
pg_restore --list vendale_<timestamp>.pgdump | head -20
3.2 Automated Snapshot (Recommended)
Configure your hosting provider's automated PostgreSQL snapshot:
- AWS RDS / Aurora: Enable automated backups with 7–30 day retention; enable continuous WAL for point-in-time recovery.
- Supabase: Enable daily backups (Pro plan) or pg_basebackup for self-hosted.
- DigitalOcean Managed PG: Enable automated daily backups.
3.3 WAL Archiving (Point-In-Time Recovery)
For workloads with frequent writes (broadcasts, media ingestion):
# postgresql.conf additions wal_level = replica archive_mode = on archive_command = 'aws s3 cp %p s3://<backup-bucket>/wal/%f'
Verify archiving is active:
SELECT pg_walfile_name(pg_current_wal_lsn()), archived_count FROM pg_stat_archiver;
---
4. Object Storage Backup
4.1 Bucket Versioning
Enable versioning on the production media bucket to allow recovery of accidentally deleted or overwritten objects. Vendale never overwrites objects under the same key (tenant-scoped opaque keys are content-addressed), but accidental operator deletion or storage-side corruption can be recovered via versioning.
# AWS S3 aws s3api put-bucket-versioning \ --bucket vendale-production \ --versioning-configuration Status=Enabled # MinIO mc version enable myminio/vendale-production
4.2 Cross-Region Replication (Recommended)
Configure cross-region replication for disaster recovery:
# AWS S3 — set up replication rule in the source bucket # Target: s3://<backup-region-bucket> # Replication time: 15 minutes SLA
4.3 Backup Verification
Monthly: randomly select 5 media asset storage keys from media_assets where download_state = 'downloaded' and verify each object exists in the production bucket.
from app.services.media_storage import ProductionObjectMediaStorage # Verify exists for a sample of storage_key values
---
5. Recovery Procedure
5.1 Database Restore From Full Dump
# 1. Create a blank target database (if starting fresh) createdb --host=<host> --username=<user> vendale # 2. Restore pg_restore \ --host=<host> \ --port=5432 \ --username=<user> \ --dbname=vendale \ --no-owner \ --no-privileges \ vendale_<timestamp>.pgdump # 3. Verify the restored schema matches the expected Alembic head export DATABASE_URL="postgresql+asyncpg://<user>:<pass>@<host>:5432/thread_crm" python -m alembic current # Must match the production migration head (e.g., f9a1b2c3d4e5)
5.2 Database Restore From Point-In-Time
Using PostgreSQL WAL recovery to restore to a specific timestamp:
# recovery.conf (PostgreSQL 12 and earlier) or postgresql.conf (13+) restore_command = 'aws s3 cp s3://<backup-bucket>/wal/%f %p' recovery_target_time = '2026-06-20 14:30:00+00' recovery_target_action = promote
After recovery, confirm alembic current matches expected head before starting the application.
5.3 Object Storage Restore
For accidentally deleted or corrupted objects, restore from versioned backup:
# AWS S3 — restore a specific version of an object aws s3api copy-object \ --bucket vendale-production \ --copy-source "vendale-production/<storage-key>?versionId=<version-id>" \ --key <storage-key>
For unrecoverable media bytes (no version, no replica), preserve the media_assets row and mark download_state = 'unavailable' rather than deleting the message history.
5.4 Full Application Recovery Sequence
Execute in this order after restoring database and object storage:
- Restore PostgreSQL — full dump or point-in-time as above.
- Restore object storage — verify critical media objects are accessible.
- Restore environment configuration — load from secrets manager; never use development values.
- Confirm
WHATSAPP_CREDENTIAL_ENCRYPTION_KEY— must match the value used when tokens were stored. - Apply Alembic migrations if needed —
python -m alembic upgrade head. - Start backend — verify
/healthand/readyreturn 200. - Start workers —
whatsapp_media_workerandwhatsapp_broadcast_worker. - Verify webhook endpoint — confirm Meta can reach
/webhooks/meta/whatsappand the challenge passes. - Send a controlled test message — confirm outbound WhatsApp provider path is live.
- Verify a broadcast — confirm worker claims jobs and sends template messages.
---
6. Disaster Recovery Targets
The following targets apply once production is activated. They are targets, not guarantees, and require the infrastructure described above to be in place.
| Scenario | Target |
|---|---|
| Database corruption (WAL replay) | Recovery Point: 5 minutes; Recovery Time: 30 minutes |
| Database host failure (snapshot) | Recovery Point: 24 hours; Recovery Time: 1 hour |
| Accidental data deletion | Recovery Point: 24 hours (versioned bucket) |
| Application code rollback | Recovery Time: 10 minutes (git checkout + redeploy) |
| Encryption key loss | Unrecoverable without offline key copy; access tokens must be re-entered |
---
7. Webhook Replay
Webhook deliveries and events are idempotent. The meta_webhook_deliveries table uses payload_hash (SHA-256 of raw JSON) as a deduplication key. Replayed provider events:
- Produce a second
meta_webhook_deliveriesrow withskipped=trueand no duplicate message/status processing. - Do not duplicate messages, conversations, or customer identities.
- Do not regress delivery statuses when the same WAMID is delivered again with the same status.
If recovery requires replaying a range of Meta webhook events:
- Re-deliver the raw payloads to
POST /webhooks/meta/whatsappwith validX-Hub-Signature-256headers. - The backend will skip already-processed deliveries and process only new ones.
- Monitor
meta_webhook_deliveries.skippedandmeta_webhook_events.skippedcolumns to confirm deduplication.
---
8. Backup Testing Schedule
| Test | Frequency |
|---|---|
| Full database restore to staging | Monthly |
| Media object verification (random sample) | Monthly |
| Point-in-time recovery drill | Quarterly |
| Full application recovery drill (database + storage + workers) | Quarterly |
| Encryption key recovery confirmation (confirm offline copy is accessible) | Semi-annually |
Practical guides live in the help center; release history in the changelog.