Skip to main content

Backup and restore one tenant

Outcome

A logical pg_dump of one tenant, and a documented restore path that goes to a parallel database first — never overwriting the live one.

Prerequisites

  • PLATFORM_ADMIN.
  • pg_dump and pg_restore on PATH at the source server's major version.
  • Key Vault read access for the tenant secret.
  • Brief read_only window OK (or pre-arranged with the customer for longer dumps).

When to use which backup

PurposeTool
Disaster recovery, server-wide rollbackAzure Backup PITR (Postgres Flexible Server) — see PITR restore
Move one tenant to another servermove-tenant
Single-tenant export (auditor request, pre-destructive change, slug change)pg_dump — this chapter

Backup flow

Steps — backup

  1. Resolve the tenant connection string from Key Vault.

    TENANT=acme
    SECRET_NAME="tenant-db-${TENANT}"
    CONN=$(az keyvault secret show --vault-name $KV --name $SECRET_NAME \
    --query value -o tsv)
  2. Flip the tenant to read_only (recommended for dumps longer than a few seconds).

    pnpm --filter @rcm/rcm-core tenant-status \
    --slug $TENANT --to read_only --reason "pg_dump snapshot"

    See Suspend or read-only a tenant.

  3. Run pg_dump in the custom format. Exclude pg_boss.* to keep the artifact small — the queue tables don't carry PHI worth restoring later.

    pg_dump --dbname="$CONN" \
    --format=custom \
    --exclude-schema=pg_boss \
    --file="${TENANT}-$(date -u +%Y%m%dT%H%M%SZ).dump"
  4. Restore the tenant to active.

    pnpm --filter @rcm/rcm-core tenant-status \
    --slug $TENANT --to active --reason "pg_dump complete"

Steps — restore

The restore always lands in a parallel database. Never restore over the live one.

  1. Provision a sibling tenant DB with a -restore-<timestamp> slug:

    pnpm --filter @rcm/rcm-core provision-tenant \
    --slug acme-restore-20260427t1500z \
    --display-name "Acme (restore)" \
    --admin-email ops@medsuite.example

    (Or provision via the UI and skip this step if you'd rather restore in place — see step 4.)

  2. Run pg_restore against the new tenant's connection string.

    RESTORE_CONN=$(az keyvault secret show --vault-name $KV \
    --name tenant-db-acme-restore-20260427t1500z --query value -o tsv)

    pg_restore --dbname="$RESTORE_CONN" \
    --no-owner --clean --if-exists \
    ./acme-20260427T1500Z.dump
  3. Validate by spot-checking key PHI tables and confirming row counts match the source. The same parity probe used by move-tenant covers the canonical canary tables:

    SELECT
    (SELECT count(*) FROM identity.organization) AS org_count,
    (SELECT count(*) FROM security.app_user) AS user_count,
    (SELECT count(*) FROM members.member) AS member_count;
  4. Cut over (only if you're rebuilding the live tenant from this dump): update identity.tenant.db_config_ref to the restored Key Vault secret. The next resolver cache miss picks up the new DB. For an immediate cutover, flip the tenant read_only first, restart the rcm-core fleet, then flip active again.

    Retain the original (now-stale) DB for at least 7 days before drop, in case you need to roll back.

Validation

CheckExpected
Dump file sizeReasonable for tenant volume; not 0 bytes
pg_restore exit code0
Parity probe row countsMatch source within expected drift
Restored DB readable via psqlYes; key tables populate

Troubleshooting

SymptomCauseFix
pg_dump: error: query failed: ERROR: permission denied for schema XTenant secret is the app role, not the admin roleUse the server-level admin connection string (resolve db_server.admin_secret_ref).
pg_restore: error: could not execute query: ERROR: relation "..." already existsRestore target already has the schemaPass --clean --if-exists (already in the example).
Dump file is hugepg_boss jobs not excluded, or member detail tables hotConfirm --exclude-schema=pg_boss. For large per-tenant volumes, consider PITR instead.
Read traffic still served from old DB after cutoverResolver pool cachingRolling-restart the fleet, or wait for TENANT_METADATA_CACHE_TTL_MS (default 30 s).

Next

1.5 — Migration fan-out across tenants