Skip to content

Backup and Restore

Ankit Upadhyay edited this page Aug 13, 2026 · 1 revision

Backup and restore

This page states what openrunic provides for backup and restore today (nothing), what a deployment must therefore do with standard tooling, and the properties of the data model that a restore has to preserve. It is for anyone planning to operate this.

Current state

openrunic ships no backup or restore tooling. There is no backup command, no scheduled job, no restore verification, and no documentation in the repository. This is not a gap in the wiki; it is a gap in the product.

Until that changes, backup is entirely a deployment concern, handled with standard PostgreSQL and object-storage tooling.

What has to be backed up

Two stores, and both are required. A restore of one without the other produces a system that looks consistent and is not.

PostgreSQL holds every row. There is nothing else: no secondary database, no search index to rebuild from, no event store.

Object storage holds every byte. The schema keeps binary content out of Postgres deliberately, so several tables carry storage keys pointing outward:

Table Column What is out there
Document storageKey Uploads, scans, inbound faxes, generated PDFs
DiagnosticReport rawStorageKey The original result message or PDF
Remittance rawStorageKey The original remittance file
Statement pdfStorageKey The generated statement

A database restored to a point where a document row exists but its object does not is a chart with a missing result. Document carries sha256 and byteSize, which makes that detectable after a restore rather than at the moment a clinician opens the file.

Doing it with standard tooling

# Logical dump, custom format, parallel-restorable
pg_dump --format=custom --file=openrunic-$(date -u +%Y%m%dT%H%M%SZ).dump "$DATABASE_URL"

# Restore into an empty database
pg_restore --dbname="$DATABASE_URL" --clean --if-exists openrunic-<timestamp>.dump

For anything with a recovery-point objective tighter than the dump interval, use continuous archiving with write-ahead-log shipping rather than periodic dumps.

Point pg_dump at the direct connection, not through a pooler. That is the same reason prisma.config.ts prefers DIRECT_URL.

Back up the object store with the same retention and the same schedule, and record the pair as a single restore point. Two independently scheduled backups do not compose into a consistent restore.

Properties a restore must preserve

The data model has three characteristics that a restore interacts with, and knowing them turns a scary restore into a boring one.

The audit chain survives a restore. createdAt and updatedAt are deliberately excluded from the audit hash, so a row rewritten by a restore with a different write timestamp still verifies. This was a design decision, not an accident, and it is the difference between a chain that survives disaster recovery and one that reports a false tamper alert the first time it is exercised.

After any restore, verify:

verifyAuditChain(events)   // { valid: true, checked: n } or a structured reason

A seq-not-contiguous result after a restore means events were lost between the last backup and the failure. That is real information, and it is worth recording as part of the incident rather than only as a technical failure.

Ids are UUIDv7, minted in application code. There are no sequences to reset after a restore, and no risk of a restored database re-issuing ids that already exist elsewhere.

Clinical rows are never hard-deleted. Correction is a status transition plus an audit event. A restore therefore does not resurrect deleted clinical data, because there was none; it restores rows whose status says what happened to them.

Testing the restore

An untested backup is not a backup. At minimum, on a schedule:

  1. Restore the most recent dump into a scratch database.
  2. Run prisma migrate status against it and confirm the history matches the deployed application's expectations.
  3. Run verifyAuditChain over the full chain for each tenant and assert it is valid.
  4. Spot-check that a sample of storage keys resolves in the object store, and that the SHA-256 matches.
  5. Record how long the whole thing took. That number, not the backup's existence, is your recovery time objective.

What should ship

Recording this so the gap is legible rather than implied. A credible self-host story needs automated encrypted backups, a restore that is one command with a tested path, and a periodic restore verification that fails loudly. Treating install, upgrade, backup, and restore as product surfaces with tests, rather than as wiki pages, is a stated goal of the project. None of it exists yet.

Related pages

Clone this wiki locally