One file to run, one file to keep
The tally further up charges the conventional stack a moving part for MySQL's backup and
restore story, which would be a cheap point to score if this server had none of its own. It
has. A backup is one .zap archive holding the three things a site actually is:
the files under its root, its configuration, and its database as a consistent snapshot. Any
one of the three travels alone if that is what you asked for.
$ zervo backup [--config PATH] [--out FILE] [--only project,config,sql] [HOSTNAME...]
$ zervo restore [--config PATH] [--yes[=BLOCK,...]] ARCHIVE [HOSTNAME...]
Neither command needs a running server, and neither shells out to anything: no external
tar, xz, zstd or sqlite3 is involved at any
point. The database block is taken with VACUUM INTO over a read-only connection,
never a filesystem copy of a live WAL database. That is why this is a feature and not a
runbook line telling you to copy a directory. A WAL database copied while
it is being written is not a backup; it is a file that resembles one.
What one archive holds
project
the file tree under the site's root: CAST, Lua, styles, assets
config
server.lua, and the site's own config.lua
sql
the SQLite database, snapshotted through VACUUM INTO
Restoring into an empty machine asks nothing, because there is nothing to overwrite: the
site is rebuilt from the configuration block, the files land at the root it recorded and the
snapshot at its data directory. Restoring over a site that already exists asks once, and asks
again per block only if that first answer was no.
Which is the part worth stopping on, because it is not really a backup feature at all.
A site the target already configures is restored to the target's own root and data
directory; a site it has never heard of falls back to the paths the archive recorded. So
the same file does two jobs that are usually two different pieces of tooling: it rolls a
live site back, and it stands a project up on a machine that has never seen it. Copy the
binary, copy the archive, restore, start. There is nothing to install first, because there
is nothing else to install. That is the honest reason a project moves between a laptop, a
staging box and a server here without a migration plan, and it falls out of the format
rather than being a feature somebody had to add on top of it.
Site example.com already exists.
Restore everything from this backup? [Y/n] n
Restore project? [Y/n] n
Restore config? [Y/n] y
Restore sql? [Y/n] n
A partial archive needs no flag on the way back. The manifest is the first entry and is read
before a single byte is unpacked, and it records which of the three blocks are present and
where each belongs. It is written in GBLN, the typed-record format these tools share, and
zervo carries its own reader for it, so an archive does not need JSON to describe itself.
format<s16>(zervo-backup)
format_version<u8>(1)
created_at<s32>(2026-07-25T16:05:28Z)
sites[
{ hostname<s256>(example.com) has_project<u8>(1) has_config<u8>(1) has_sql<u8>(1) … }
]
- Restore writes files, it does not reload a running server. Restarting the site's workers stays the operator's step.
- The project block is additive, not a mirror. Archived files overwrite their counterparts and files added since the backup survive. There is no mode that makes the tree match the archive exactly.
- Only the database is snapshotted. The file tree is copied as it is found, so a site taking uploads during a backup can produce a tree the database disagrees with. Quiet hours, or a brief pause, remain the operator's call.
- A restore that would need a question refuses rather than assumes. With no terminal attached it aborts and names
--yes, so a scheduled job cannot silently overwrite a live site.
- File ownership is not preserved. Restored files belong to whoever ran the command; permissions come from the archive.