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×:
| Database | Compressed replica | Restore |
|---|---|---|
| 288 MB | 65 MB | ~0.4 s |
| 1.1 GB | 260 MB | ~1.3 s |
| 2.3 GB | 520 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:
| Flag | Restores |
|---|---|
| (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:00ZIntegrity & 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) andrestore --timestampresolve against the stored objects directly, independently of the advisoryCURRENTpointer — so an attacker with backend write access who repointsCURRENTto 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 verifywarns whenCURRENTisn't the newest backup, so a repointed pointer is still visible.