Pre-release. bakelite is unreleased and still under active testing — docs and behaviour may change without notice.

Restore

You want a copy of your database back — either the latest state, or how it looked at some earlier moment. That's what restore does. It writes to a fresh file and never touches your live database, so you can run it any time, even while bakelite's still backing up.

(Under the hood it replays the base snapshot plus the page-changes recorded after it. Restoring a long history from a high-latency backend like S3 still stays fast, because it downloads upcoming chunks in parallel while applying the current one, and it's memory-bounded, applying one chunk at a time.)

How long it takes

Restore is bounded by I/O and the integrity pass, and it scales with database size — there's no "replay the whole history" penalty. It reads the base snapshot plus only the change-sets after it (prefetching upcoming chunks in parallel while applying the current one, at bounded memory), then runs PRAGMA integrity_check over the result.

Some reference points — Apple M4, local-file backend, latest restore, integrity-checked, best of three runs; test data compresses ~4.5×:

DatabaseCompressed replicaRestore
288 MB65 MB~0.4 s
1.1 GB260 MB~1.3 s
2.3 GB520 MB~18 s

A couple of things stand out. While the database fits in the OS page cache, restore runs at roughly a second per gigabyte; once it outgrows cache (the 2.3 GB row) the write-and-verify passes go disk-bound and integrity_check dominates — so plan against your own hardware, not the fastest row. And these are local numbers: on S3 the added term is downloading the compressed replica (the middle column), largely hidden behind the parallel apply — e.g. a 260 MB replica at 100 MB/s is a couple of seconds of download, mostly overlapped.

Restore time is what your incident plan actually depends on, so measure it on your own hardware and backend rather than trusting a table from someone else's machine:

time bakelite restore --config /etc/bakelite/bakelite.toml --db app \
  --output /tmp/restore-drill.db        # the time includes the integrity check

bakelite verify --deep runs the same end-to-end drill (restore + integrity_check) against your real replica — wire it into cron and you'll know if that number ever drifts.

Choosing a target

restore resolves a target one of these ways:

FlagRestores
(none)The latest captured state.
--timestamp <when>The state as of that moment.
--index <n>Up to and including a specific restore-point index. Advanced.

The selectors are mutually exclusive. --timestamp accepts an RFC3339 instant (2026-05-25T12:00:00Z), the keywords now or yesterday, or a relative <N><unit> ago form (30m ago, 2 hours ago, 7d ago) with units s/sec/secs, m/min/mins, h/hr/hrs, d/day/days. The --index value comes from bakelite list --verbose.

Point-in-time restore gives you a consistent snapshot of the database as it was at the chosen moment — it works across bakelite's internal checkpoints, and the restored file is sized to match the database at that point. Run bakelite list --db <name> to see the timestamps you can restore to.

# Latest state.
bakelite restore --config /etc/bakelite/bakelite.toml --db app \
  --output /tmp/restored.db

# Point in time.
bakelite restore --config /etc/bakelite/bakelite.toml --db app \
  --output /tmp/at.db --timestamp 2026-05-25T12:00:00Z

Integrity & safe swaps

Restore writes a standalone database file and runs PRAGMA integrity_check. It never touches the live database — always restore to a scratch path, verify, then swap it into place.

Restore on boot

For the disposable-container pattern — seed the database from the replica when the volume is empty, otherwise leave it alone — omit --output (restore writes the configured path) and add the guards: --if-db-not-exists (no-op if the file is already there) and --if-replica-exists (no-op on a brand-new deployment with nothing backed up). Both exit 0 when they skip, so the entrypoint can run restore unconditionally. See restore in the CLI reference.

As it goes, restore also checks every object's recorded object_hash, so a corrupted or substituted object is refused rather than applied. On an encrypted replica each object is sealed with XChaCha20-Poly1305 and bound to its own identity (database + position) as authenticated data, so if the backend relocates, swaps, or rolls back an object, decryption of that object fails and restore stops rather than applying it.

bakelite restore --config /etc/bakelite/bakelite.toml --db app --output /tmp/at.db
sqlite3 /tmp/at.db 'PRAGMA integrity_check;'   # extra check, if you like
# stop the app, swap, restart:
mv /tmp/at.db /var/lib/app/app.db

Rollback note. Both plain restore (latest) and restore --timestamp resolve against the stored objects directly, independently of the advisory CURRENT pointer — so an attacker with backend write access who repoints CURRENT to an older backup does not roll a restore back. What restore can't reconstruct is a backup an attacker outright deletes; on an encrypted replica the per-object identity binding (above) makes tampering with the objects themselves detectable. bakelite verify warns when CURRENT isn't the newest backup, so a repointed pointer is still visible.