Backup and restore
| Method | PostgreSQL with pgBackRest: full, differential and incremental backups plus continuous WAL archiving, encrypted, to two repositories (one off-site) |
| Drill | make backup-drill (deploy/backup/restore-drill.sh): point-in-time restore of a copy of the demo database, verified table by table |
| Last drill | 2026-10-30, all checks passed (§4) |
1. What is protected
| Data | Where | Protection | Recovery |
|---|---|---|---|
| Application database: ledger, orders, customers, audit, blockchain intents with their signed envelopes, indexer projections, scan schema | PostgreSQL | Synchronous replica + pgBackRest (two encrypted repositories) | Replica promotion, or point-in-time restore (§3) |
| Account manifest, configuration | Git / deployment secrets | Version control and the secret store | Redeploy |
| Stellar chain data | The network itself, our RPC nodes | Not backed up: nodes resync; the ledger is public and final | STELLAR_RPC_RUNBOOK.md |
| Keys | HSM / KMS, human signers' hardware keys | Never in database backups (../security/KEY_MANAGEMENT.md) | KEY_MANAGEMENT.md |
| Monitoring data | Prometheus, Loki | Not backed up (operational) | — |
The chain is the second copy of every token balance: after a restore, reconciliation compares the restored ledger with the chain. Intents whose transactions applied after the restore point are found by their MEMO_HASH (the indexer re-reads the ledgers after its restored cursor) and must be resolved before trading resumes (§3 step 5).
2. Schedule and targets
| Item | Value |
|---|---|
| Full backup | Weekly; retain 4 locally, 8 off-site |
| Differential / incremental | Daily / hourly |
| WAL archiving | Continuous, archive_timeout = 60 s |
| Verification | pgbackrest check after every backup; restore drill quarterly and after every PostgreSQL major upgrade |
| RPO | 0 with the synchronous replica; ≤ 60 s from the repositories |
| RTO | Replica promotion: minutes. Repository restore: measured 19 s for the demo database (§4); size-proportional |
3. Restore procedure (point in time)
- Stop the workers and APIs (no writes). Pause trading is implicit: the gate state ages out.
- Choose the target time (before the incident) and restore into an empty data directory:
pgbackrest --stanza=dara --type=time --target=<time> --target-action=promote restore. - Start PostgreSQL, then one worker only: startup verification checks the manifest against the chain.
- Let the indexer catch up from its restored cursor; it re-reads every ledger since, so every on-chain movement after the restore point appears again, linked to its intent by MEMO_HASH.
- Run reconciliation. Differences after a restore are expected exactly where intents applied on-chain after the
restore point; resolve each as
CHAIN_OPERATION_UNRESOLVED(INCIDENT_RESPONSE.md) before trading resumes. - Start the APIs; the gate reopens after the hold-down.
4. Drill results
deploy/backup/restore-drill-result.md keeps every run. 2026-10-30, demo database (602 rows, 37 tables): full
backup 22 s (3.8 MB compressed and encrypted); restore to the target time and promotion 19 s; every table identical
to the target state; the write after the target absent; the table dropped after the target back; no unbalanced
ledger transaction; append-only and balance triggers present; indexer cursor restored.