pgbr
Reference

Database Schema

The tables in pgbr's metadata database and how they relate.

pgbr's own Postgres holds users, connections, job history, schedules, and storage settings. Schema is defined with Drizzle in packages/db/src/schema/; migrations live in packages/db/drizzle/ and are applied automatically by the dashboard's entrypoint on boot.

This is pgbr's metadata database, not a database you back up. Losing it doesn't lose your artifacts — but it loses everything that tells you what those artifacts are.

Relationships

Auth tables

Managed by Better Auth.

users

ColumnTypeNotes
iduuidPrimary key
nametext
emailtextUnique
email_verifiedbooleanDefaults false; pgbr doesn't verify
usernametextUnique
display_usernametext
created_at / updated_attimestamp

There is no role column. "The first user is the admin" means they got in first, not that they hold a privilege — every user is equivalent.

sessions, accounts, verifications

Session tokens with expiry and IP/user-agent, credential records (hashed passwords live in accounts.password), and verification tokens. All cascade on user deletion, and each is indexed on the column it's looked up by.

databases

A saved connection.

ColumnTypeNotes
idtextPrimary key, UUID
user_iduuidusers.id, set null on delete
nametextUnique per user (databases_user_id_name_unique)
urltextEncrypted iv:authTag:ciphertext
backup_countintegerLifetime successful backups; only increments
created_at / updated_attimestamp

name is unique per owner, matching what the create and update actions check, so two users can each have a database named production. Reusing one of your own names is rejected with "Database name already exists" rather than a constraint violation surfaced as a generic error.

backup_jobs

ColumnTypeNotes
idtextPrimary key. A UUID, or a sanitized BullMQ scheduler ID
database_idtextdatabases.id, set null on delete
user_iduuidusers.id, set null on delete
schedule_idtextbackup_schedules.id, set null on delete
database_nametextDenormalized — survives the database being deleted
statustextpending / running / completed / failed
storage_keytextLogical object key, independent of any mount
flagsjsonbThe exact flags this dump ran with
errortextThe tool's stderr on failure
sizebigintBytes, counted during upload
started_at / completed_attimestamp

The FKs all set null rather than cascade, which is what lets a backup outlive its database and still be downloadable. database_name is denormalized for the same reason — after the database is gone, it's the only record of what was dumped.

flags being stored per job is what makes an artifact self-describing: the restore path reads the recorded format to decide whether the artifact needs expanding.

size is a bigint. It was a 32-bit integer through 2.3.0, which capped at 2,147,483,647 bytes (~2 GB) and failed any larger backup at the very last step — after the dump and upload had both succeeded. Migration 0004 widens the column in place; existing rows are unaffected.

backup_schedules

ColumnTypeNotes
idtextPrimary key. Also the BullMQ scheduler key
user_iduuidusers.id, cascade on delete
database_idtextdatabases.id, cascade. Immutable after creation
nametext
cron_expressiontext5 fields
timezonetextIANA. Defaults UTC
enabledbooleanDefaults true
flagsjsonbpg_dump flags for each run
keep_lastintegerRetention. Null = keep everything

Schedules cascade where jobs set null: a schedule with no database is meaningless, but a backup without one is still an artifact you might need.

The row is the source of truth; the BullMQ scheduler keyed by id is derived and rebuilt on worker boot.

restore_jobs

ColumnTypeNotes
idtextPrimary key, UUID
database_idtextThe target. → databases.id, set null
user_iduuidusers.id, set null
database_nametextDenormalized target name
statustextSame four values
storage_keytextThe tracked backup's key, or a custom upload's
flagsjsonb
errortext
started_at / completed_attimestamp

No size — a restore consumes an artifact, it doesn't produce one.

migration_jobs

ColumnTypeNotes
idtextPrimary key, UUID
user_iduuidusers.id, set null
source_database_id / target_database_idtextNull for custom URLs
source_database_url / target_database_urltextEncrypted
source_database_name / target_database_nametextNull for custom URLs
backup_flags / restore_flagsjsonbBoth sides
statustext
errortextFiltered stderr, truncated at 3,000 chars
sizebigintBytes streamed from pg_dump into pg_restore
started_at / completed_attimestamp

Both URLs are stored encrypted, including custom ones you never saved as connections — a migration record shouldn't be a plaintext credential leak.

A migration streams straight from pg_dump into pg_restore and never lands an artifact, so size is counted as the bytes cross the pipe rather than measured from a file. It records the dump's size, not the space used in the target, and is recorded even when the restore side fails.

storage_settings

A singleton row with id default.

ColumnTypeNotes
idtextPrimary key, always default
endpointtext
regiontext
buckettext
access_key_idtextPlaintext — not a secret
secret_access_keytextEncrypted. Never returned to the client
force_path_stylebooleanDefaults true

Its presence is what makes the settings page report source: "settings" instead of "environment". Absent, everything falls back to STORAGE_* and then the built-in defaults.

Migration history

MigrationChange
0000_breezy_lady_bullseyeInitial schema
0001_late_blink
0002_add_storage_key_and_settingsAdded storage_key and storage_settings — the move to object storage
0003_drop_backup_pathDropped the old filesystem backup_path

0002/0003 are split deliberately: adding the new column and dropping the old one in one step makes drizzle-kit generate ask whether it's a rename, which it can't do in a non-TTY shell. Add first, drop second.

Conventions

  • snake_case column naming, configured on the Drizzle client
  • timestampscreated_at / updated_at, with updated_at auto-touched
  • Encrypted columns store iv:authTag:ciphertext, hex-encoded
  • Job IDs are text, not uuid, because scheduled backups use sanitized BullMQ scheduler IDs
  • Denormalized names on job rows, so history survives its subject

On this page