Skip to main content

Back up and restore Prism's databases

Prism keeps everything it remembers in Postgres, and nowhere else. There is no second store, no file on a volume that matters, and nothing outside your network. So backing Prism up is backing up two logical databases, and restoring it is restoring them.

This page is the procedure and the check at the end. It matters more than it used to: since 0.14.0 the appliance holds your own definitions — what your words mean, written by your administrators — and those are the one thing here that nobody else has a copy of. Ingested GitHub, Jira and Router data can be walked again from the systems it came from. A definition cannot.

What is in there​

Two logical databases, both on the instance the chart points at.

DatabaseChart valueWhat it holdsIf you lost it
prism — the datapondpostgres.external.database, default prismIngested pull requests, issues and spend; the source registry and its credentials; your definitions and their revision history; app users and sign-in state; saved prompts, feedback reports and the answer archiveThe ingested data can be walked again. The definitions, the feedback and the archive cannot
runner — the agent runtime's statepostgres.external.runnerDatabase, default runnerThe agent, its MCP profiles, sessions and conversation stateChats in progress are lost, and Prism provisions the agent and its stream connections again on the next start

Neither is optional and both are on the same instance, so in practice you back up the instance.

If you set postgres.bundled=false — which is what you should be doing for anything beyond a pilot — this is your managed instance, and your organisation's existing backup policy already covers it. Read the rest of this page anyway for the restore check and for what has to be restored together.

If you set postgres.bundled=true, Prism runs a single in-cluster Postgres on a PersistentVolumeClaim, and nothing backs it up for you. The PVC is the whole of your data and a cluster that loses it loses everything above.

Take a backup​

The dump has to be consistent across the whole database, not table by table: several of Prism's tables are written together in one transaction, and a dump assembled from separate reads can catch them half-applied. pg_dump gives you that by default — it reads in a single transaction — so the rule is simply to dump the database, never a list of tables.

# The datapond. Point PGHOST/PGUSER/PGPASSWORD at the same instance the chart does.
pg_dump --format=custom --file=prism-datapond-$(date +%F).dump prism

# The runner's own state.
pg_dump --format=custom --file=prism-runner-$(date +%F).dump runner

For the bundled instance, run it through the pod:

kubectl exec -n <namespace> statefulset/<release>-postgres -- \
pg_dump --format=custom --username postgres prism \
> prism-datapond-$(date +%F).dump

No -t. A custom-format dump is binary, and a TTY translates line endings in it — kubectl exec -it produces a file that looks fine, copies fine, and fails at pg_restore weeks later when you need it.

Three things about these files:

  • They contain credentials. Every source credential an administrator entered is in sources.credential_ciphertext, encrypted under the installation's ENCRYPTION_KEY. The dump cannot be read without that key — and a restore is useless without it, so back the key up too, wherever you keep your Secrets.
  • They contain your people's data. Directory rows, questions people asked and answers Prism gave. Treat a dump as you would treat the database.
  • They do not contain the images or the chart. Those are immutable and come from your registry; a restore needs the same version, which is the next section.

Restore​

Restore into a Prism that is not running, and at the same version the dump came from. Both matter.

Stopping it is two steps, not one, and the second is the one that is easy to miss. Scale the two long-running writers down:

kubectl scale -n <namespace> --replicas=0 \
deployment/<release>-app deployment/<release>-agent-runner

Then suspend the ingest schedules. They are CronJobs, so scaling deployments does nothing to them, and a tick that fires while you are restoring writes into the database you are restoring — into tables pg_restore --clean is dropping as it goes. This is the failure that leaves a restore looking successful and the data subtly wrong.

# Every schedule this release owns, whatever your sources are called.
kubectl get cronjob -n <namespace> -l app.kubernetes.io/instance=<release> \
-o name | xargs -r kubectl patch -n <namespace> \
-p '{"spec":{"suspend":true}}'

# And wait for anything already running to finish.
kubectl get jobs -n <namespace> -l app.kubernetes.io/instance=<release>

Un-suspend them ("suspend":false) after the check at the end, not before.

<release> is your Helm release name — prism unless you chose another, so prism-app and prism-agent-runner on a default install.

Then load each database:

pg_restore --clean --if-exists --dbname prism prism-datapond-2026-09-09.dump
pg_restore --clean --if-exists --dbname runner prism-runner-2026-09-09.dump

On a managed instance, expect the extension line to be the one that fails. The runner database uses pgvector, and its dump therefore carries CREATE EXTENSION vector — which a non-superuser cannot run unless the extension is already allowed. On Azure Database for PostgreSQL that means vector in the azure.extensions server parameter, the same setting the install needed in the first place. If your role cannot create it, install the extension as an administrator first and add --exit-on-error so a restore that cannot complete stops rather than leaving you a half-loaded database that starts and answers wrongly.

Then scale back up and un-suspend the schedules. The app recreates anything it needs on boot and rewrites nothing it found.

Restore the whole database, not a subset of the tables. Prism's definitions are three tables — the revisions, the words each definition answers to, and the installation's own policy — written together and meaningless apart. A restore that brought back the revisions and not the words would leave every definition active and unfindable, with nothing reporting it.

Check that it worked​

Do both. The first proves the rows are there; the second proves Prism agrees.

1. The active definitions are the ones you had. Every definition has exactly one active revision, and this lists them:

psql prism -c "SELECT name, kind, revision, activated_at
FROM definitions
WHERE activated_at IS NOT NULL AND withdrawn_at IS NULL
ORDER BY name"

The count and the revision numbers should match what Admin → Definitions showed before the backup. A definition whose revision has gone backwards means you restored an older dump than you meant to.

2. Prism answers in your words. Open Admin → Definitions and confirm the list matches, then ask a question that uses one of your defined words. The answer names the definition and revision it used, and that sentence is the end-to-end proof: the row was restored, the appliance read it, and the answer was interpreted through it.

If either check disagrees, do not re-run the restore on top — scale down, drop and recreate the database, and restore again into an empty one.

Upgrades, downgrades and the schema​

In the datapond — the database your definitions, your sources and your answers live in — Prism creates its tables itself, on boot, and only ever adds. There is no migration to run, no migration to reverse, and nothing is dropped or rewritten by an upgrade.

The runner database is the agent runtime's own and migrates itself on boot to whatever version is running. Treat it as belonging to the version that is installed: restore it from a dump taken on that version, and if you only have an older one, restore the datapond and let the runner rebuild rather than forcing an older runner dump under a newer runner. Nothing a reader cares about is lost that way — the agent, its profiles and its stream connections are provisioned again on the next start.

That gives you the supported rollback path, and it is the ordinary one:

  • Rolling forward — a new version creates any tables it needs the first time the app starts. Existing rows are untouched.
  • Rolling back with helm rollback prism — the earlier version does not know about tables a later version added, does not read them, and does not delete them. Your definitions sit there unread until you roll forward again, and then they are exactly as you left them.
  • Restoring a dump taken on a later version into an earlier one is the case to avoid. The datapond will load and the earlier app will ignore whatever it does not understand — but a definition written under a newer version may mean something that version's engine cannot execute, and the runner database makes no such promise in either direction. Restore into the version the dump came from, then upgrade.

There is one thing a rollback does not undo: a definition activated on the newer version stays active. If you rolled back because an answer changed, the lever is Admin → Definitions, where you withdraw the definition or activate an earlier revision of it — not the database.

What is not backed up here, and does not need to be​

  • Ingested source data — pull requests, issues, spend. It is in the dump, and it is also re-walkable from GitHub, Jira and the Router. A restore that lost a day of it catches up on the next ingest.
  • The images and the chart. Immutable, in your registry, pinned by version.
  • Secrets. They are Kubernetes Secrets, not Prism's, and your existing practice covers them — but ENCRYPTION_KEY is the one whose loss cannot be worked around: without it, every stored source credential in a restored dump is unreadable and has to be entered again.