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.
| Database | Chart value | What it holds | If you lost it |
|---|---|---|---|
prism — the datapond | postgres.external.database, default prism | Ingested 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 archive | The ingested data can be walked again. The definitions, the feedback and the archive cannot |
runner — the agent runtime's state | postgres.external.runnerDatabase, default runner | The agent, its MCP profiles, sessions and conversation state | Chats 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'sENCRYPTION_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
runnerdatabase 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_KEYis 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.