Databases
SafeGrd supports backups across four database engines. Backups are captured using native database tooling from a consistent snapshot where supported, then streamed, compressed, and encrypted directly on the host before being written to locked storage. No superuser privileges are required; a read-only role provides sufficient access for backups.
| Database | What is captured | The host needs |
|---|---|---|
| PostgreSQL | Schema from pg_dump, rows over binary COPY, one snapshot | pg_dump at least as new as the server |
| MySQL and MariaDB | mysqldump --single-transaction | The MySQL client |
| MongoDB | mongodump --archive | The MongoDB Database Tools |
| SQLite | One committed state, through SQLite's online backup | Nothing |
PostgreSQL
The schema comes from pg_dump: types, tables, functions, views, indexes,
constraints and triggers, exactly as Postgres describes them. The rows are copied over the
wire with binary COPY, table by table, in chunks, so a table larger than the
host's memory backs up without staging anything on disk. Both are read from one
snapshot, so a database being written to during the backup still produces rows, row
counts and sequence positions that agree.
- The host needs a
pg_dumpat least as new as the server: the PostgreSQL client package (postgresql-client-16on Debian and Ubuntu,brew install libpqon macOS). SafeGrd finds versioned installs under/usr/lib/postgresqland Homebrew's keg-onlylibpqby itself.SAFEGRD_PG_DUMPnames one explicitly. The password is passed to it in the environment, never on its command line. - If a compatible
pg_dumpbinary is not found, the backup still captures row data via binary COPY, but schema definitions will be incomplete. The CLI will log a warning, and automated Fire Drills will fail because relational constraints, views, triggers, and enum types cannot be properly reconstructed without pg_dump. - Superuser privileges are not required unless specific extensions demand them. Standard
database owner roles (such as those provisioned by AWS RDS or Cloud SQL) can back up,
drill, and restore databases. Trusted extensions like
pgcryptoandcitextcan be created directly by the owner role, while pre-installed cloud provider extensions and system tables remain untouched. Any extension requiring superuser privileges must be pre-installed in the restore target database prior to restoration. - Restores require an empty target database. A Supabase project is never empty, so a Supabase backup restores into Supabase's PostgreSQL image (how).
A drill on a database surface asserts table counts, row counts, column counts, installed extensions and that every role the schema names is in the backup. See Restore and Fire Drills.
Roles, owners and grants
The schema is dumped with each object's owner, its grants and its default privileges,
and the policies on a table name the roles they apply to. The roles those name (owners,
grantees, policy roles) are backed up as pg_dumpall --roles-only writes them,
in roles.sql beside the schema, with their password hashes. The rest of the
cluster's roles are not backed up.
- A restore creates the roles the target lacks before the schema runs, and prints each
one as
Created role. A created role is never a superuser or a replication role; its login and password are as they were, so an application role logs in on the new cluster as it did.safegrd restore --no-ownerrestores without owners and grants, so every object belongs to the restoring role; the roles are still created, because a policy cannot be restored without them. - A restore run by a role that is not a superuser skips the owners and grants it may not
set (an owner it is not a member of, a grant on an object it does not own), says how
many, and gives those objects to the restoring role. A role it creates gets no
CREATEDB,CREATEROLEorBYPASSRLS, which only a role holding them may set. Restore as a superuser to keep every owner and attribute. - The daemon's sandbox drill creates the roles the same way and drops them with the
sandbox. The role the sandbox URL connects as needs
CREATEROLEwhen the cluster lacks a role the schema names.verify --sandbox-targetkeeps what it restored, roles included, for you to look at. roles_without_passwords: trueon a surface, or--roles-without-passwordsonsafegrd backup, leaves the hashes out; a restore then creates the roles without a password. A backup role that may not readpg_authid(an RDS master user, Supabase'spostgres) gets the same, and the backup says so. A surface SafeGrd backs up for you has no switch and keeps the hashes.- The in-memory drill fails a snapshot whose schema names a role it does not carry, since no fresh cluster could load it. Snapshots taken before 2026-10-05 carry no roles; the next backup does.
Each backup uploads what changed
A PostgreSQL backup is a run of the surface's repository. Each table is one file of the run, cut into chunks of about 64 KiB, and a later run in the same month uploads and locks only the chunks that changed. A table nobody wrote, or one that only grows at the end, adds almost nothing. A table updated at random rows all over can add its whole size on every run. The snapshot detail in the console shows what each table uploaded.
Every run is a restore point: a restore brings the database back as that run read it, and
nothing between two runs. Pass --format tar, or set format: tar on a
surface, for one archive per backup instead. When you add a surface in the console,
Backup type sets this: Incremental or Full. On hosted
storage, the console can download a full backup as one file; an incremental one restores
on a host (hosted storage). A SQLite
database is incremental by default too (below). MySQL and MongoDB
backups are one archive each.
--table can be given more than once. The tables come from one run, so they are
consistent with each other, and load in one transaction that commits only if every table
matched its SHA-256 and its row count. The target's other tables stay as they are. A table
the target no longer has, such as one an agent dropped, is created as the run defined it,
with its sequences, constraints and indexes, and its sequences are set to where the run
left them. A table the target has must be empty and defined as it was. The command prints
the run's time, the tables the restored ones point at that it did not load, and the
sequences of an existing table that it did not move. restore --table public.orders
--version 3 restores the third version find --table lists.
Skip tables nobody wrote
A run reads every table, because a database has no file times to compare. On a large
database that read takes minutes and loads the server on every run.
--change-log, or change_log: true on a surface, lets a run skip each
table nothing wrote since the last run:
- It creates a
safegrdschema in the database and a trigger on each table, which logs the table and the transaction once per transaction after an insert, update, delete or truncate. The role taking the backup needs to own the tables (or holdTRIGGERon them) and be able to create a schema, so it is not the read-only role above. A table the role cannot add the trigger to is read on every run, and the backup names it. - A table is skipped only when the log shows no write to it since the last run, its definition is unchanged, and a run read it within the last 7 days. A restore of the database to an earlier point, a failover to another timeline, a renamed enum label or a server upgrade makes the next run read every table. Statistics counters are never used.
- Each transaction pays one row in the log for each table it writes, about 12 µs per table on a laptop running PostgreSQL 16. Rows are deleted once every backup that reads the log has seen them.
- PostgreSQL 13 or newer. Surfaces SafeGrd backs up itself do not offer it yet.
DROP SCHEMA safegrd CASCADE;removes the log and every trigger.
Give SafeGrd a read-only role
SafeGrd only reads your database. Give it a role of its own with no write permissions, so that a leaked backup credential cannot change or delete the data it backs up.
pg_read_all_data is built in from PostgreSQL 14. It grants SELECT
on every table, view and sequence and USAGE on every schema, including ones
created later, so a new table is covered without anyone remembering to grant it.
PostgreSQL 13 has no such role, so grant it explicitly and set the default for tables
created later:
Run the ALTER DEFAULT PRIVILEGES statement too. Without it, tables created later
are not readable by the role and are left out of future backups. pg_dump runs as
the same role. Check that the role cannot write before you go on:
safegrd doctor checks this for every PostgreSQL surface the host can open, and
safegrd claim checks the surfaces it adds. For a database SafeGrd backs up itself,
the console checks it when the connection string is saved and shows the same advice. The line Surface <id> role
warns when the role is a superuser, can create roles or databases, or can write to any table,
names up to three of those tables, and prints the two lines above as the fix. A second line,
Surface <id> row security, names the tables whose rows row-level security
hides from the role. A role that is not a superuser cannot read password hashes, so roles are
backed up without their passwords (roles).
- Row-Level Security:
pg_read_all_datadoes not automatically bypass RLS. If a table has policies, the backup holds only the rows those policies let the role see. SafeGrd checks each table before it copies it: the backup prints every table RLS applied to, with the rows copied and the table's estimated size, and the console marks the table Row-level security in the snapshot. Grant the roleBYPASSRLS, or back up as the tables' owner, to capture every row. - Large objects:
pg_read_all_dataapplies to relations rather thanpg_largeobject. If your application relies on large objects (lo_*), test a restore in advance to verify complete coverage. - Replicas: Configuring SafeGrd against a read replica offloads dump traffic from your primary writer, as backups require only read access.
Supabase
Supabase runs PostgreSQL with pre-installed schemas for auth, storage and realtime. To back it up from GitHub Actions, with no server of your own, follow Back up Supabase with GitHub Actions. Or let SafeGrd take the backup from the connection string: Back up on SafeGrd under Surfaces in the console (how it runs). Use the session pooler address below either way.
- Port 5432: Connect to the direct host,
db.[PROJECT-REF].supabase.co:5432. It has only an IPv6 address unless you buy Supabase's IPv4 add-on, so a host with no IPv6 (GitHub's runners among them) uses the session pooler instead,aws-0-[REGION].pooler.supabase.com:5432with userpostgres.[PROJECT-REF]. Do not use the transaction pooler on port 6543: it shares server connections between clients, which breaks binaryCOPYand the one snapshot the backup reads every table from. - SSL: Add
?sslmode=requireto the database URL. - Row-Level Security:
pg_read_all_datadoes not bypass RLS. Either give the backup roleBYPASSRLSor grant explicit SELECT policies on tenant tables, otherwise those tables back up with 0 rows. A project's ownpostgresuser already hasBYPASSRLS. - Restoring: A Supabase project already holds Supabase's own schemas and its
postgresuser is not a superuser, so a new project cannot take the restore either. Restore into Supabase's PostgreSQL image assupabase_admin, following the guide's restore section. Stockpostgresimages lack extensions such assupabase_vault.
Amazon RDS and Aurora (PostgreSQL)
RDS restricts superuser access, but SafeGrd only needs a read-only role.
- Back up from a read replica: Point
database_urlat your replica endpoint. SafeGrd only reads, so using a replica keeps dump traffic off your primary writer. - SSL certificates: Download Amazon's root certificate bundle (
global-bundle.pem) and pass?sslmode=verify-full&sslrootcert=/path/global-bundle.pem. - Role creation: Create a read-only role with
GRANT pg_read_all_data TO safegrd_backup;. Superuser access is not needed.
Neon
Neon is serverless Postgres with compute autosuspend.
- Use the direct endpoint: Use the direct connection string (e.g.
ep-xyz.region.aws.neon.tech:5432) without the-poolersuffix so binary streaming works. - SSL: Neon requires TLS. Add
?sslmode=requireto the URL. - Cold starts: If compute autosuspend is on, the initial connection wakes the instance. Allow a few seconds for cold starts.
MySQL and MariaDB
MySQL surfaces run mysqldump (or mariadb-dump) with
--single-transaction, which reads InnoDB tables from one consistent snapshot
without locking them. Backups include stored routines, triggers and events, with binary
columns written as hex. The output streams into the encrypted archive as it is produced.
Row counts are read from the dump stream itself, so they match the rows in the backup even
while the database is being written to.
- The host needs the MySQL client (
mysql-client, orbrew install mysql-clienton macOS). SafeGrd prefers the dump tool from the same project as the server, and MySQL's client has been tested against MariaDB 11.4 as well.SAFEGRD_MYSQLDUMPandSAFEGRD_MYSQLname them explicitly. - The password goes to the client in a private, owner-only options file that is deleted when the run ends, never on its command line.
- TLS: add
?tls=trueto the URL, or?ssl-ca=/path/ca.pemto verify the server against your own CA. - Restoring requires an empty target database and validates every table's restored row count against the snapshot manifest. Because MySQL applies DDL statements non-transactionally, a failed restore can leave partial schemas behind; drop and recreate the database before retrying.
- Database objects such as views, triggers, and stored routines preserve their original definer
accounts. Recreating an arbitrary definer requires
SET_USER_IDprivileges, which managed platforms like RDS do not grant. During restoration, SafeGrd rewrites object definers to match the restoring account and reports the number of modified definitions. - Rows up to the source server's
max_allowed_packetare backed up whole. Setmax_allowed_packeton the restore target at least as high as on the source. - Triggers created via multi-statement APIs may contain trailing semicolons that
mysqldumpexports in an invalid format. SafeGrd identifies affected triggers during backup, and sandbox drills will flag them since in-memory drills do not parse SQL syntax.
Amazon RDS and Aurora (MySQL)
Managed MySQL on RDS does not grant SUPER privileges, but SafeGrd does not need them.
- SafeGrd runs
mysqldumpwith--single-transactionand--set-gtid-purged=OFF, which works with standard user privileges. - During a restore, SafeGrd rewrites object definers (e.g.
%oradmin@%) to the restoring account so RDS does not reject them. - Backing up from an Aurora or RDS Read Replica keeps table scans off your write master.
MongoDB
MongoDB surfaces capture database archives using native mongodump --archive,
streaming data directly into the snapshot stream. Because the archive uses MongoDB's standard
format, it can be restored directly with mongorestore independently of SafeGrd. Document
counts and collection checksums generated by mongodump are validated as the stream passes.
- The host needs the MongoDB Database Tools (
mongodb-database-tools).SAFEGRD_MONGODUMPandSAFEGRD_MONGORESTOREname them explicitly. The connection string goes to them in a private, owner-only file, never on their command line. - Collections are read consistently per-collection, but point-in-time consistency across different collections is not guaranteed because database-scoped dumps do not capture global oplogs. Documents modified across collections during backup execution may reflect slightly different timestamps.
- Indexes and view definitions are preserved in the backup. Server-level users and roles are managed globally and are not included in single-database archives.
- Restores target an empty database (which can have any name) and verify document counts against the snapshot manifest. If a restore fails prematurely, drop the target database before attempting a rerun.
SQLite
That is one backup, now. For backups on a schedule, with Fire Drills, name the file as a
surface in ~/.safegrd/config.yaml and run the daemon.
Pick one of the two for a database: a surface starts its own history, so backups taken
with --database-url before it show in the console as a separate surface.
A file copied off the disk while the app writes can be torn, and in WAL mode the newest
transactions are still in the -wal file, which a copy of the database file
misses. SafeGrd copies the database through SQLite itself, inside one read transaction:
every committed transaction is in the copy, the WAL's included, and writers carry on.
- Each backup is a run of the surface's repository, like a PostgreSQL backup. The copy keeps every page where the database has it, so a run uploads and locks only the 64 KiB chunks whose pages changed. A 228 MB database with 400 rows updated since the last run uploaded 6.8 MB. Inserts into an index on random keys, such as UUIDs or email addresses, touch pages all over that index and upload more.
--format tar, orformat: taron a surface, takes one archive per backup instead, compacted withVACUUM INTO. Once decrypted and unpacked it is a SQLite file any client opens.- In WAL mode a backup does not block writers. In rollback-journal mode writers wait while
the copy is taken.
safegrd doctorsays which mode the database is in, andPRAGMA journal_mode=WAL;switches it. - The copy goes through
PRAGMA integrity_checkbefore a byte of it is uploaded. A damaged database is not stored, and the backup says why. Earlier snapshots are not touched. - The copy is written to an owner-only directory under the system temporary directory and
deleted after the upload. It needs free space about the size of the database; set
TMPDIRto use another disk. - A restore writes a new file and never overwrites one:
safegrd restore --snapshot <id> --target sqlite:///var/lib/app/restored.db. The file and its-walmust not exist. The copy is checked withPRAGMA integrity_checkand then renamed into place. A run restores whole:--tableand--to-sqlare for PostgreSQL. - To put a restored file in place of the live one: stop the app, move the old
app.dband anyapp.db-walandapp.db-shmaside, moverestored.dbtoapp.db, and start the app. The restored file is in rollback-journal mode; runPRAGMA journal_mode=WAL;on it first if the app expects WAL. - The SQLite engine is built into
safegrd, so the host needs nosqlite3.
Where the connection URL comes from
PostgreSQL, MySQL and MongoDB connect with a URL. SQLite has no secret: a surface names its file as database_url: sqlite:///var/lib/app/app.db, with no credential block.
You choose where each URL lives, per database, and the two can be mixed on one host:
- Stored locally on the host: The surface's
credentialblock saysfrom: envwith an environment variable'sname,from: commandwith a command that outputs the URL (such asop readorvault kv get), orfrom: filewith apath. The control plane only learns the variable or command reference, never the credential itself. This is the default mode. - Managed centrally by SafeGrd: When the block says
from: safegrd, SafeGrd stores the database URL sealed. The host fetches it over HTTPS when the surface backs up and keeps it in memory for that run. Enter it in the setup wizard or under Nodes → Credential in the console.
A surface takes its credential from one place. If it has both from: safegrd and
a database_url on the host, the backup stops with a configuration conflict
error. Having SafeGrd hold the database credential does not make it hold the encryption
key. Those are separate choices (Security and key custody).
What a Fire Drill checks
| Database | What the drill checks |
|---|---|
| PostgreSQL | Validates table counts, row counts, installed extensions and the
roles the schema names (roles, owners and grants) against the
snapshot manifest while verifying binary COPY streams. In-memory drills verify data integrity
without running a live database server. Sandbox drills (using --sandbox-target or the daemon's
drill.sandbox_url) restore into a temporary database to verify schema definitions and table constraints. On a plan with sandbox drills, a surface with no drill.sandbox_url restores into a throwaway cluster the daemon starts from the host's PostgreSQL server (Daemon). |
| MySQL and MariaDB | Confirms dump completeness by checking terminal markers, verifying table structures,
and validating row counts against recorded manifest metadata. In-memory drills check dump integrity;
sandbox drills (via --sandbox-target mysql://… or drill.sandbox_url) load the
dump into a scratch database, verify table counts, and clean up the database upon completion. |
| MongoDB | Verifies archive stream termination, matching document counts and collection checksums
against values recorded by mongodump. Sandbox drills restore the archive into a temporary database
using mongorestore, verify collection counts, and drop the temporary collections afterward. |
| SQLite | Executes a complete test restore: the database is written to a temporary location, verified
with PRAGMA integrity_check, and confirmed against table row counts before the temporary file is removed. |
Every surface's drill, files and mail included, is compared in the table on the Surfaces page.