Docs / Surfaces

Surfaces

A surface is one thing worth protecting: a database, a file tree or a mailbox. Each Fire Drill tests one surface. All surfaces share one pipeline: stream, compress with zstandard, encrypt to your age public key and write to locked storage. They differ only in how the data is read at the start and checked at the end.

Plaintext never crosses a third-party network: the stream is encrypted on the host that read it. It never touches disk either, with one exception: a SQLite surface is copied to a private, owner-only temporary file on that same host first (SQLite), and the file is deleted as soon as it is uploaded.

The surfaces

SurfaceWhat is capturedThe host needs
PostgreSQLSchema from pg_dump, rows over binary COPY, one snapshotpg_dump at least as new as the server
MySQL and MariaDBmysqldump --single-transactionThe MySQL client
MongoDBmongodump --archiveThe MongoDB Database Tools
SQLiteOne committed state, through VACUUM INTONothing
FilesA directory tree, incremental: each run uploads only what changedRead access to the tree
EmailEvery message of an IMAP mailbox, as RFC 5322The mailbox's password or app password

Something you need that is not in the table (Microsoft 365 mail, MSSQL, Redis, ClickHouse, Elasticsearch, Windows hosts, Kubernetes volumes): request a surface.

What a Fire Drill proves, per surface

A Fire Drill always pulls the snapshot back from your bucket and decrypts it with your key. What it checks after that depends on the surface.

SurfaceWhat the drill checks
PostgreSQLThe table count and every table's row count match the manifest, each table's binary COPY stream is readable, and the extensions are present. By default this is done in memory, with no database running anywhere, which checks the data but never runs the schema. Only a sandbox drill proves the schema loads: with --sandbox-target the snapshot is restored into a scratch database you name and counted there, and the daemon does the same for a surface with drill.sandbox_url. On a plan with sandbox drills, a surface without one restores into a throwaway cluster the daemon starts from the host's PostgreSQL server (Daemon).
MySQL and MariaDBThe dump is complete, ending with its completion line. Every table the manifest names is in it, with exactly the recorded number of rows. In memory, that checks the data. A sandbox drill (--sandbox-target mysql://… or drill.sandbox_url) loads the dump into an empty database, counts every table, and empties the sandbox again.
MongoDBEvery collection and view in the archive reached its end. Every collection's documents match the checksum mongodump wrote and the count the manifest recorded. A sandbox drill restores into an empty database with mongorestore, counts every collection, and drops them again.
SQLiteA real restore, every time: the database is written out, opened by the SQLite engine, passes PRAGMA integrity_check, and every table's row count matches the manifest. The file is deleted after.
FilesThe whole snapshot restores into a scratch directory, every file matching its SHA-256, and the digest recomputed from the restored files matches the one recorded at backup time. A --format tar archive is checked in memory against the manifest sealed inside it.
EmailThe message count matches the manifest, every message parses as RFC 5322 mail, and every message's SHA-256 matches. Nothing is written to disk.

None of these replaces a rehearsed restore, which proves that a person can get the data back. A drill proves that each snapshot can be read back whole, every time, without anyone remembering to check.

Running several surfaces on one host

Every example above is a one-shot command. To protect more than one thing from a single host, describe the surfaces in the config file and let the daemon run them on a schedule:

surfaces:
  - id: "app-primary"
    type: "postgres"
    schedule: "@daily"
    credential:
      from: env
      name: "APP_DATABASE_URL"
  - id: "user-uploads"
    type: "files"
    schedule: "6h"
    roots: ["/var/www/uploads"]

Each entry registers as its own node and counts as one protected surface against your plan quota. id must be stable: it is how state, locks and snapshot history are keyed. A config with no surfaces: list still loads as a single implicit Postgres surface, so no existing install needs an edit. The full shape is in Daemon.

Adding a surface to a host that is already running

The host's config is the one list of what it protects, so a surface is added there, in one of two ways:

safegrd claim
# the daemon reads its config when it starts
safegrd daemon restart

Either way, restart the daemon afterwards, or run safegrd daemon run --once to take the first backups now.