Backups
Verified against v0.21.0 · internal/db/migrate.go, internal/plugin/secrets.go, internal/poker/publication/connections.go, internal/db/migrations/0057_publication_connections.sql, internal/plugin/runtime.go, internal/standup/webhook.go, cmd/parley/main.go, docs/security/README.md
Nothing in Parley backs itself up. pg_dump is the whole story; there is no
built-in scheduler, no retention policy, and no encryption unless you add one.
Back up before every upgrade
Section titled “Back up before every upgrade”Take a dump immediately before every docker compose up -d or helm upgrade
that changes Parley’s version, not just before major ones. Migrations run
automatically at boot, and some are forward-only with no down
path — the backup
taken a second before the new version starts is the only way back if a
migration does something you didn’t expect.
Dump it
Section titled “Dump it”docker compose exec db pg_dump -U parley parley > parley-backup.sqlEncrypt the dump
Section titled “Encrypt the dump”A dump contains meeting content and every space’s room code in readable form. Session tokens are hashed rather than stored as reusable bearer values, but restoring their rows can make a previously revoked bearer value valid again. Plugin, webhook signing and GitHub publication credentials are ciphertext with a separate key dependency. Treat the dump as a secret from the moment it exists, both at rest and wherever it travels (object storage, a laptop, an offsite copy).
With age:
docker compose exec db pg_dump -U parley parley | age -r age1yourpublickeyhere > parley-backup.sql.age
# restoreage -d -i key.txt parley-backup.sql.age > parley-backup.sqlWith gpg, symmetric encryption uses a passphrase, which still needs protected
recovery custody:
docker compose exec db pg_dump -U parley parley | gpg --symmetric --cipher-algo AES256 -o parley-backup.sql.gpg
# restoregpg --decrypt parley-backup.sql.gpg > parley-backup.sqlEither way, keep the passphrase or private key off the machine that holds the dump — an encrypted backup sitting next to its own key protects against nothing.
Preserve the recovery dependencies
Section titled “Preserve the recovery dependencies”Record the backup’s timestamp, database/schema and Parley versions, configured
plugins/bundles, and key IDs needed to read encrypted secrets. Keep the matching
PLUGIN_SECRET_KEY and, where needed during rotation,
PLUGIN_SECRET_KEY_PREVIOUS in separately protected recovery custody. These
environment secrets are not included in pg_dump. Older backups may need an
older key even after all live rows have been resealed; retain the necessary
key material for the permitted backup lifetime, with restricted access.
Also preserve the dump-encryption key, deployment configuration, trusted plugin signer keys/bundles, and IdP/TLS dependencies. Record who may retrieve each dependency and how access works if the normal host or IdP is unavailable. Do not put keys into the evidence register or next to a broadly readable dump.
Without a matching key, restoring the database does not recover plugin or webhook secrets. Re-enter or rotate affected provider/signing credentials through supported configuration after recovery; a fresh key cannot decrypt old ciphertext. Review plugin key rotation before removing an old key.
Restore it
Section titled “Restore it”Into a fresh stack:
Keep every Parley replica stopped and start only Postgres for the restore and reconciliation below. Keep ingress and outbound effects restricted through validation. Set those network restrictions before starting Parley. A scratch restore must not send queued plugin jobs or webhook deliveries to production recipients.
Parley has no worker-pause configuration: starting the server also starts its queue and outbox workers. Network isolation alone cannot prevent queued plugin jobs from changing local state under restored grants or consuming delivery attempts.
docker compose up -d dbcat parley-backup.sql | docker compose exec -T db psql -U parley parleyReconcile access before reopening
Section titled “Reconcile access before reopening”A backup also rolls authorization and queues back to its timestamp. Compare it with protected records of subsequent account/membership removals, org revocation tombstones, token and signed-link revocations, plugin grant changes, connector removals, and deletions. Reapply those decisions while only Postgres is running, before starting any Parley replica. If there is no reliable change record, choose a conservative reset of affected sessions/links/credentials and require renewed authorization; do not assume the restored state is current.
Deleting restored session-token rows alone does not prevent a restored active signed link from minting another token. Reconcile links as well as tokens, membership and grants. Review pending outbox/jobs and webhook deliveries for stale authority and duplicate effects, and resolve queued work that must not run, while Parley is still stopped. Confirm that the configured keys can decrypt retained secrets before any delivery is attempted.
Restoring can also resurrect deleted content. Apply the deployment’s retention and deletion decisions to live records and recovery copies, and document any remaining backup-retention limits. Parley does not supply automatic post-restore reconciliation; the operator owns this procedure.
After database reconciliation, start all replicas against the reconciled store with the network restrictions still in place:
docker compose up -dVerify old HTTP and WebSocket credentials and revoked link redemptions are refused before reopening access or egress.
On Kubernetes
Section titled “On Kubernetes”pg_dump still does the job, but nothing runs it for you. Schedule it as a
CronJob in the same namespace that execs into (or connects directly to) the
Postgres pod/service and writes the encrypted dump to object storage — the
Kubernetes page and your Postgres operator’s own
docs (CloudNativePG, Zalando, Crunchy) are the right starting point, since the
exact command depends on which one manages your database. If you run your own
Postgres StatefulSet rather than an operator, the CronJob runs the same
pg_dump | age pipeline above from a pod instead of a laptop.
Run a restore drill
Section titled “Run a restore drill”A backup you have never restored is a hope, not a backup. Periodically:
- Spin up a scratch stack (a second Compose project or a throwaway namespace).
- Restore the most recent dump with only Postgres running, then reconcile post-backup authorization, deletions, and pending queues before starting Parley.
- Start Parley with network restrictions in place. Confirm the app boots against it and a space you recognize is there.
- Check representative sessions, votes/entries, plugin state, and encrypted secrets with the matching recovery keys. Confirm post-backup revocations and deletions have been reconciled, and old credentials/links are refused.
- Record the tested backup age, versions/digest, key availability (no key values), data/access checks, elapsed recovery time, failures, owner and date. Compare the result with your chosen RPO/RTO.
- Tear the scratch stack down, including its sensitive recovered data.
Do this after any schema-changing upgrade, not just on a calendar — that’s the moment a restore is most likely to surface a problem while it’s still cheap to find one.
What this shape actually recovers
Section titled “What this shape actually recovers”A pg_dump backup has a bluntly honest RPO/RTO, worth stating rather than
assuming:
- RPO (how much you can lose) is the interval between dumps. A backup taken once a day means up to a day of standups, estimates, and roster changes are gone if you have to restore. Dump more often if that’s not acceptable — nothing in this shape requires daily.
- RTO (how long you’re down) includes provisioning, retrieving and decrypting the backup, restoring the database and keys, reconciling access and deletions, validating the result, and reopening the service. Measure it in a representative drill; database size and an unprovisioned target can dominate it. Set an objective from your deployment’s needs rather than treating an estimated restart time as a recovery commitment.
Point-in-time recovery, replication, and continuous WAL archiving are real
options if this RPO isn’t good enough, but they are a Postgres operations
topic in their own right and outside what this page covers — they layer on
top of the same parley database this page backs up.
Publication credentials after restore
Section titled “Publication credentials after restore”GitHub publication credentials use the existing current/previous keys and
parley secrets reseal. Ciphertext always binds connection ID, org, space,
room and principal; legacy unbound ciphertext is refused even in recovery.
The dedicated table participates in stale counts, compare-and-set reseal and
the final locked check. Corrupt or wrong-scope rows prevent zero-remaining
recovery; re-enter or revoke them before removing the previous key.
A restored backup may restore a removed PAT connection or old grants. Before exposure, reconcile membership, facilitator and connection revocations against post-backup changes, and revoke the upstream token when needed. Keep delivery paused until scope and keys are checked. Reconcile retained unknown identities against GitHub rather than clearing or recreating them. Never put reusable PATs in support artifacts, exports or restore logs.