100% Self-HostedFree to Install, Free to Get HostedYour School, Your DataOpen Source ForeverNo Per-Student Fees, Ever100% Self-HostedFree to Install, Free to Get HostedYour School, Your DataOpen Source ForeverNo Per-Student Fees, Ever
OpenRegistro
All docs

Backups and restore

Backup

# JSON driver (default)
STORAGE_DRIVER=json DATA_DIR=./data pnpm backup

# Postgres
STORAGE_DRIVER=postgres DATABASE_URL=postgres://... pnpm backup

This runs scripts/backup.sh, which writes a timestamped archive into backups/ (openregistro-json-<timestamp>.tar.gz or openregistro-postgres-<timestamp>.sql, the latter via pg_dump). The backups/ directory is created if it doesn't exist; add it to your own off-host backup rotation (it is gitignored, OpenRegistro does not upload backups anywhere itself).

Restore

STORAGE_DRIVER=json DATA_DIR=./data pnpm restore backups/openregistro-json-<timestamp>.tar.gz
STORAGE_DRIVER=postgres DATABASE_URL=postgres://... pnpm restore backups/openregistro-postgres-<timestamp>.sql

Restoring the JSON driver overwrites DATA_DIR's current contents. Restoring Postgres runs the dump through psql inside a single transaction. Stop the app before restoring to avoid writes racing the restore.

Moving from JSON to Postgres

A one-way migration copies every record from a JSON data directory into a Postgres database through the same StorageDriver contract both drivers implement:

# 1. Apply migrations to the (empty) target database first.
DATABASE_URL=postgres://... pnpm --filter @openregistro/db db:migrate

# 2. Copy the data.
DATA_DIR=./data DATABASE_URL=postgres://... pnpm migrate:json-to-postgres

This is implemented in apps/web/scripts/migrate-json-to-postgres.ts. Technical note: the StorageDriver.create() methods always generate a fresh id server-side (ids are never accepted from the caller - see the repository factories under packages/db/src), so a naive record-by-record copy would break every foreign key. The script instead builds an old-id -> new-id map as it creates each entity and substitutes mapped ids into every dependent record, in dependency order (schools, then school years, terms, classes, subjects, users, and so on down to the audit log). Absence justifications that were already approved/rejected in the source data have their review decision replayed through absenceJustifications.review() after creation, since the create-only API only accepts a "pending" initial state - the one side effect of this is that the migrated record's reviewedAt timestamp becomes the migration time rather than the original review time.

Run this once against a freshly migrated (empty) Postgres database - it does not de-duplicate against existing rows if run twice.

Demo instance reset

STORAGE_DRIVER=json DATA_DIR=./data pnpm demo:reset

Wipes storage (deletes DATA_DIR, or drops and recreates the public schema for Postgres) and reseeds the fictional demo dataset, then marks the installer complete so the instance is immediately usable again without re-running the setup wizard. Intended for public demo deployments that should reset to a known state on a schedule.

Found an issue with this page? Edit it on GitHub.