# Backup & restore (/docs/operations/backup-restore)



Ductor's durability model is simple: **PostgreSQL is the source of truth**, and
**Redis/Dragonfly holds rebuildable operational state**. Your backup strategy
follows directly from that split.

Two things are **Tier-0 recovery assets** — lose either and no database restore
can bring the data back:

1. The **database backup** (Postgres).
2. The **connector/payload master key** — and, for BYOK tenants, the **tenant
   KEK custody**. Losing the active master key with no rotation copy in the
   keyring makes every stored credential and any AEAD-encrypted payload
   permanently unrecoverable. The encryption is one-way by design. See
   [Key management](/docs/operations/key-management) and
   [Key custody](/docs/auth/key-custody).

## What holds what [#what-holds-what]

| Store                           | Contents                                                                                                                                                                | Recovery posture                                                                    |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| PostgreSQL                      | Workflow runs (`eec_workflow_run`), step attempts (`eec_workflow_step_attempt`), pools, recipients, rules, tenants, connections (encrypted), idempotency records, audit | **Must be backed up.** Losing it loses durable state.                               |
| Redis / Dragonfly               | Cache, flow-control counters, tiered queue, pub/sub                                                                                                                     | Rebuildable, but its loss is disruptive (in-flight queue items, capacity counters). |
| Object storage (if archival on) | Archived history/visibility streams                                                                                                                                     | Back up per your storage backend's policy.                                          |
| The connector encryption key    | AEAD master key (config/secret, **not** in the database)                                                                                                                | Back up separately and securely — see the warning below.                            |

The durable runtime tables above share an `eec_` prefix — short for *Enterprise
Eventing Core*, Ductor's event, task, and workflow store.

<Callout type="error" title="Back up the encryption key separately — it is Tier-0">
  Connector credentials in Postgres are encrypted with
  `connector.encryption_key`, which lives in your config/secret store — **not** in
  the database. A database restore is useless for connections without the matching
  key. Store the key (and any `connector.rotation_keys`) in your secret manager
  with its own backup and rotation policy, at the same retention class as the
  database backup. If you run the AEAD payload codec, the same key protects
  payload bytes; for BYOK tenants the tenant KEK custody is Tier-0 too.
</Callout>

## Backing up PostgreSQL [#backing-up-postgresql]

Use standard PostgreSQL tooling — Ductor adds nothing special. The schema is
plain Postgres (TimescaleDB extensions where used).

### Logical dump [#logical-dump]

```bash
pg_dump --format=custom --no-owner \
  "postgres://ductor:$PGPASS@host:5432/ductor" \
  -f ductor-$(date +%F).dump
```

### On Dokploy [#on-dokploy]

Dokploy can schedule Postgres backups of the `timescale` service directly from
its UI — configure a backup schedule and destination there.

### On Kubernetes [#on-kubernetes]

Back up your managed/operator-run Postgres with its native mechanism
(base backups + WAL archiving for point-in-time recovery, or scheduled
`pg_dump`). Ductor connects to an external database in production Helm setups, so
backups are that database's responsibility.

<Callout title="Prefer PITR for production">
  A nightly `pg_dump` bounds data loss to a day. For tighter RPO, use continuous
  archiving (base backup + WAL) so you can restore to any point in time.
</Callout>

## Restoring PostgreSQL [#restoring-postgresql]

<Steps>
  <Step>
    **Stop or scale down Ductor** so nothing writes during the restore.
  </Step>

  <Step>
    **Restore the dump** into a clean database:

    ```bash
    pg_restore --clean --no-owner \
      -d "postgres://ductor:$PGPASS@host:5432/ductor" \
      ductor-2026-07-11.dump
    ```
  </Step>

  <Step>
    **Reconcile the schema.** The restored data matches the schema at backup time.
    Run migrations up to the version your binary expects, then confirm:

    ```bash
    ductor migrate up
    ductor schema status      # expect: schema=ready
    ```
  </Step>

  <Step>
    **Ensure the connector encryption key** in your config/secret store matches the
    key that encrypted the restored connections.
  </Step>

  <Step>
    **Start Ductor** and verify `/ready` returns `200`.
  </Step>
</Steps>

## Redis / Dragonfly [#redis--dragonfly]

Cache and flow-control state rebuilds itself as traffic resumes, so a cold cache
is a performance dip, not data loss. The tiered queue is the exception — items
in flight when Redis is lost may need to be re-driven. If you want a warm restart,
back up Dragonfly with its snapshot mechanism (the compose volume is
`dragonfly_data`); otherwise let it repopulate. Either way, Ductor's
[coordinator/worker model](/docs/concepts/coordinator-workers) means progress
already committed to Postgres is safe — a run resumes from durable state.

## Additional recovery requirements [#additional-recovery-requirements]

Two subsystems need consistency guarantees beyond a whole-database dump:

* **Billing ledger** — the
  `stripe_*` tables (`stripe_wallet_balance`, `stripe_charge_event`,
  `stripe_customer_mapping`, `stripe_payment_config`) hold local wallet
  balances and payment policy that **Stripe cannot reconstruct** (see
  [Billing & Usage → Stripe integration](/docs/billing/stripe) for what the
  wallet and charge subsystem is). Take a
  PITR-consistent pre-change snapshot with a single
  `pg_dump --serializable-deferrable` of those tables, quiesce billing writes,
  and restore into an isolated cluster before promoting. Treat the four-table
  snapshot and the master-key backup as the same retention class.

* **Multi-region replication failover**
  — promote a follower cluster to leader when the primary region is
  unreachable. Verify the candidate has drained its inbox
  (`replication_inbox_lag_seconds` \~ 0), quiesce the leader, then flip the
  leader bit in each tenant's replication policy. Outbox/inbox lag forensics
  (`replication_dispatcher_lag_seconds`, `replication_inbox_apply_failures_total`)
  tell you whether a follower is safe to promote. Failover is irreversible
  without a second swap.

## Restore drill [#restore-drill]

Rehearse restores before you need them:

* [ ] Take a fresh `pg_dump` and restore it into a scratch database.
* [ ] Run `ductor migrate up` and `ductor schema status` against it.
* [ ] Point a throwaway Ductor at it with the real connector encryption key and
  confirm a connection decrypts.
* [ ] Confirm `/ready` returns `200`.
