Recovery
PostgreSQL and the object bucket are the durable stores. Reward balance can be recomputed from the ledger. Payment truth can be reread from Stripe. Signed URLs cannot, and do not need to be restored.
Backups
| Store | Backup | Retention target |
|---|---|---|
| PostgreSQL | Daily logical dump, plus volume snapshots when the host provides them | 30 daily dumps, until the host plan says otherwise |
| Object storage | Versioning on the private bucket, or a replicated bucket | Enough to restore an original deleted by mistake |
| Secrets | The secret manager's own recovery | Separate from the database dump |
| Documentation | The git repository | With the code |
Dumps are encrypted and stored off the database machine. A dump that includes password hashes and encrypted MFA secrets is still a secret.
Restore drill
Run a restore onto a non-production instance at least once before launch, and again after the first production booking exists. The drill checks that a customer, an order, a gallery access row, and a ledger sum match the source. Record the time required. That time is the recovery expectation until a later drill improves it.
Payments after an outage
- Restore PostgreSQL to the chosen point.
- Start the API and worker.
- Run reconciliation for orders still
PENDING_PAYMENTorREFUND_PENDING. - Reconciliation reads Stripe and applies the same transaction as a webhook.
- The webhook unique key ignores events that were already stored.
Do not mark orders paid from a spreadsheet or from customer screenshots. The provider is the source for money movement. The database is the source for whether the business has already applied that movement.
Photographs
Restore object versions for any key referenced by photos.storage_key or photos.thumbnail_key. If a thumbnail is missing and the original is present, queue image.thumbnail again. Access status is in PostgreSQL. Restoring files does not approve a gallery.
Rewards
Do not restore a balance from a cache. After the database restore, SUM(reward_ledger.amount_cents) is the balance. If a qualifying refund was applied at the provider and missing from the ledger, reconciliation posts a reversal. It does not edit the credit row.
Refresh tokens
Restored refresh-token hashes become valid again until expires_at. If the dump may have leaked, revoke them by setting revoked_at and require customers and the photographer to sign in again. Rotate JWT_ACCESS_SECRET and the storage secret if the dump or the host was exposed. Access tokens are not stored, so they expire on their own.