Boom Online Ordering
Transaction Durability, Redundancy & Restore Architecture

Never lose a transaction. Restore any client to any second.

A disaster-recovery design for a money-critical ordering platform: continuous point-in-time recovery, a redundant failover database, immutable offsite copies, and granular per-client / per-system restore — built on your DigitalOcean + Backblaze B2 stack.

BOO is transaction-critical · Higher tier than the SaaS apps · 2026-07-09
Why BOO Is Different

Payment systems don't get "hourly backups"

Your 15 SaaS apps can tolerate losing an hour of data in a worst case — annoying, not catastrophic. Boom Online Ordering cannot. Every lost order is lost revenue for a restaurant client, a customer charged with no record, or a payout dispute. The bar is defined by two numbers — set them deliberately:

RPO — Recovery Point Objective
How much data you can afford to lose (measured in time).
Hourly dump = up to 60 min lost. BOO target: seconds.
RTO — Recovery Time Objective
How long you can afford to be down during recovery.
Rebuild-from-dump = hours. BOO target: minutes.
🔴 First architectural move: isolate BOO's database. Do not keep transaction data in the shared multi-tenant SaaS Postgres. Give BOO its own hardened, dedicated database instance with this DR pipeline. Mixing money data into the shared hourly dump is the single biggest risk in your current setup.
Current vs Target

The gap you're closing

CapabilityToday (shared hourly dump)BOO target
Max data loss (RPO)Up to 60 minutes~Seconds (PITR)
Downtime on DB failure (RTO)Hours (manual rebuild)Minutes (auto-failover)
Node/droplet diesOutage + possible lossStandby promotes, stays up
Restore to an exact momentNo — only hourly snapshotsYes — any second in the window
Restore ONE clientManual, slowScripted extract from PITR
Ransomware / corruptionDumps could be encrypted tooImmutable, locked offsite copy
The Architecture

Defense in depth — six layers

No single mechanism protects against everything. A node failure, a bad migration, a ransomware attack, and a provider outage are four different disasters needing four different answers. Stack them.

L1
HA
Protects against: a node dying

Redundant primary + hot standby (failover)

A second Postgres node continuously replicates the primary via streaming replication. If the primary droplet dies, the standby is promoted automatically. Keeps BOO online with near-zero downtime. Automated with Patroni, or handled for you by a managed database's standby node.

L2
PITR
Protects against: bad data, dropped table, human error

Continuous WAL archiving → point-in-time recovery

Postgres's Write-Ahead Log is shipped to B2 continuously (every few seconds). You can restore the database to any exact second — e.g. "the moment before the bad deploy at 14:32:07." This is what takes RPO from 60 minutes down to seconds. Tool: pgBackRest or WAL-G.

L3
BASE
Protects against: total loss of the primary

Scheduled base backups + retention

Full + incremental base backups (nightly full, hourly incremental) give WAL a starting point to replay from. pgBackRest manages retention automatically (e.g. keep 14 fulls). Stored in B2.

L4
LOCK
Protects against: ransomware & malicious deletion

Immutable offsite copy (B2 Object Lock)

A copy of backups written with Object Lock can't be altered or deleted — even by a compromised key — until its retention expires. This is your insurance against an attacker who reaches your infrastructure and tries to destroy the backups too.

L5
GEO
Protects against: a whole provider/region failing

Cross-provider / cross-region copy

Primary DB on DigitalOcean, backups on Backblaze B2 = already two providers (good). Optionally replicate the B2 bucket to a second region so a regional B2 outage still leaves you recoverable.

L0
PROVE
Protects against: backups that silently don't work

Tested restores + monitoring & alerting

Automated monthly restore drills into a scratch DB. Alerts if WAL archiving falls behind, a backup fails, or replication lag grows. An untested backup is not a backup.

How to Build It

Two ways to get there

Both deliver the six layers. The difference is how much database operations you run yourself. For a money system, my recommendation is to buy the hard parts (HA + failover) and self-run only the independent backups.

Recommended for BOO

Managed primary + independent offsite

DigitalOcean Managed Postgres for the BOO database + pgBackRest → B2 for backups you control.

  • HA standby node + automatic failover, built in (L1)
  • Managed PITR + daily backups within DO (L2/L3)
  • pgBackRest → B2 = a second, independent, immutable backup lineage you own (L2/L3/L4) — never trust one provider's backups alone
  • Far less ops burden; DO handles patching, failover, replication
  • Costs more than a raw droplet (worth it for money data)
Full control

Self-hosted HA on droplets

Patroni + etcd across 2–3 droplets + pgBackRest → B2.

  • Total control, lowest infra cost
  • Same six layers, all self-run
  • You own failover logic, quorum, patching, on-call
  • Higher risk of misconfiguration on the thing that holds money
  • Fits your Coolify ethos, but DB HA is the one place "managed" usually wins
⚖️ The principle either way: keep at least two independent backup systems for BOO. Even a managed DB's backups can be lost to an account issue — so pgBackRest → B2 (which you control, with Object Lock) is non-negotiable regardless of which primary you choose.
Granular Restore

APer-client & per-system restore points

"Restore one client to a point in time" needs a strategy, because in a shared database you can't PITR just one tenant. Three complementary techniques:

1. PITR + tenant extract (the workhorse)

Restore the whole BOO database to a scratch instance at the target timestamp, then copy just that client's rows back into production, filtered by tenant_id. Scripted, this is a 10-minute operation for any single client, to any second.

# Restore BOO to the exact moment before an incident
pgbackrest --stanza=boo --type=time \
   --target="2026-07-09 14:32:00" restore   # → scratch DB

# Extract one client and re-import to production
pg_dump --data-only \
   -t orders -t transactions -t payments \
   --where="tenant_id='rlg-bailbonds'" scratch_db \
   | psql production_boo

2. Scheduled per-tenant logical exports (fast single-client rollback)

Nightly, dump each active client's data to its own object in B2 (b2:boo-backups/tenants/<id>/<date>.sql.gz). When one client needs a quick rollback you restore just their file — no full-cluster restore needed. Cheap, and gives clean per-client restore points.

3. Tenant isolation model — decide deliberately

ModelPer-client restoreTrade-off
Shared DB, tenant_id column (current)Via extract (above)Simplest ops; restore needs filtering
Schema-per-tenantCleaner — dump/restore one schemaModerate complexity; good middle ground
Database-per-tenantTrivial — restore one DBBest isolation; heaviest ops at scale

For BOO specifically, schema-per-tenant is often the sweet spot: strong isolation and simple per-client restore, without a database explosion. Worth evaluating before the client count grows.

Transaction Integrity

BApplication-level safeguards

Backups protect the database; these protect the correctness of money even when systems hiccup. Layer them on top:

SafeguardWhat it buys you
Stripe as payment source-of-truthPayment records also live in Stripe. If your DB is ever behind, reconcile against Stripe's ledger — you can rebuild payment state from it. Never rely solely on your own DB for money truth.
Append-only transaction ledgerWrite every order/payment event as an immutable row (never UPDATE/DELETE). Corruption or a bad edit can be replayed and audited. This is the "black box recorder" for BOO.
Idempotency keysEvery order/charge carries a unique key so a retry after a network blip never double-charges or double-creates. Essential during failover moments.
Nightly reconciliation jobAutomated compare of BOO orders ↔ Stripe charges ↔ payouts. Flags any drift the next morning, before a client notices.
Outbox pattern for integrationsOrder → Rewardee points, BOO → external hooks: write the intent to an outbox table in the same transaction, deliver async. Nothing gets lost if a downstream is briefly down.
When Something Breaks

CRestore runbook

ScenarioResponseLayer
Primary DB droplet diesStandby auto-promotes; verify app reconnects; rebuild a new standbyL1
Bad migration / dropped tablePITR restore to the second before it; extract affected tables backL2
One client's data corruptedRestore their nightly tenant export, or PITR-extract by tenant_idL2/per-tenant
Ransomware on infrastructureRebuild from Object-Locked immutable B2 copy the attacker couldn't touchL4
DigitalOcean region outageRestore from B2 to a droplet in another region/providerL5
Payment/DB mismatchReconcile against Stripe ledger; replay from append-only ledgerApp
Implementation Roadmap

Build it in order of risk-reduction

Phase 1

Isolate BOO onto its own database

Separate transaction data from the shared SaaS Postgres. Managed DO Postgres recommended for the primary (buys you L1 HA immediately).

Phase 2

Stand up pgBackRest → B2 (PITR)

Continuous WAL archiving + nightly full / hourly incremental to a dedicated boo-backups bucket with Object Lock. This is the biggest single RPO win (60 min → seconds).

Phase 3

Add the app-level safeguards

Append-only ledger, idempotency keys, and the nightly Stripe reconciliation job. Protects correctness independent of the DB.

Phase 4

Per-tenant exports + restore scripts

Nightly per-client dumps and a tested one-command per-client restore. Evaluate schema-per-tenant if isolation needs grow.

Phase 5

Monitoring, alerting & monthly restore drills

Alert on WAL lag / failed backups / replication lag. Automated monthly restore test into a scratch DB. Prove RTO/RPO are actually met.

Done When

BOO durability checklist