ADR-0089: VayuOS One-Click Update & Full Backup/Restore
Status: Accepted
Date: 2026-06-26
Author: @johalputt
Context
VayuPress already shipped a secure self-update engine (internal/update,
ADR-0064): it checks GitHub for a newer release, downloads the binary plus its
.sha256 and .sig, verifies the SHA-256 checksum and an Ed25519 signature
against a pinned release public key, backs up the database, and atomically
replaces the running binary. It also shipped a database backup helper
(CreateBackup) that archives the SQLite file and its WAL/SHM sidecars.
Two gaps remained for a single-VPS operator who never wants to touch a shell:
- The apply path was CLI-only.
vayupress update applyrequired SSH access, and it never restarted the process — the operator still had to runsystemctl restart. There was no way to update from VayuOS. - There was no whole-site export/import.
CreateBackupproduced an archive on the server's disk but offered no download, no upload-and-restore, and no consistency guarantee against a live WAL. Moving a site between machines, or rolling back content+settings, meant manual file surgery.
The request: a production-grade VayuOS surface where the operator updates VayuPress in one click — binary and everything — with no command line and no half-finished state, and can back up the entire database and settings to one compressed file and import/export it again, with no size limit.
Decision
1. Expose the existing engine through VayuOS, keeping every gate
A new Update & Backup panel (/os/update, admin_os_update.go) drives the
unchanged internal/update engine. The security contract of ADR-0064 is
preserved exactly:
- Apply still calls
PreflightApply: it refuses unlessVAYU_SELFUPDATE_ENABLED=true, a pinnedVAYU_RELEASE_PUBKEYis present, and the system is not in read-only / quarantined / maintenance mode. - The release is verified (checksum + Ed25519) before any byte is written, and the database is backed up before the binary is swapped.
These are one-time deployment settings, not per-update steps, so once armed the
operator truly gets one click. Every action is admin-role gated,
CSRF-protected on writes, and recorded in both the WORM audit_log and the
update_history table.
2. Finish the job: in-process self-restart
internal/update/restart.go adds Relaunch / ScheduleRestart, which re-exec
the freshly written binary in place via execve(2) after a graceful
PRAGMA wal_checkpoint(TRUNCATE) and DB close. The PID and service identity are
preserved; if the re-exec ever fails, the process exits cleanly so a supervisor
(systemd Restart=always, Docker restart policy) brings it back. This is the
piece that makes the update truly hands-free — no systemctl restart step.
3. Whole-site snapshots: consistent, streamed, unbounded
internal/update/snapshot.go adds a self-describing snapshot format — a single
.tar.gz containing manifest.json, a portable settings.json dump, and the
database (vayupress.db). Because all site settings live in the site_settings
table, one DB copy captures content and settings.
- Consistency: the DB is copied with SQLite
VACUUM INTO, which takes a read snapshot and writes a fully checkpointed, defragmented standalone file. No torn pages, and no need to ship-wal/-shmsidecars. - No size limit, constant memory: export streams
manifest → settings → DBstraight to the HTTP response withio.Copy; import streams the upload throughgzip/tarreaders to disk via aMultipartReader(neverParseMultipartForm). The handlers lift the server read/write deadlines withhttp.ResponseControllerso large transfers don't time out. - Integrity: the manifest records the DB SHA-256; restore recomputes and
compares it, then validates the staged file is a real, intact VayuPress
database (
PRAGMA integrity_check+ presence ofschema_migrations,articles,site_settings).
4. Crash-safe restore via a startup swap
A restore never mutates the open live database. The validated incoming DB is
staged as <db>.pending-restore; ApplyPendingRestore — called at process
start before the DB is opened — takes an automatic safety backup of the
current DB, then atomically renames the staged file into place and clears stale
sidecars. The import handler triggers a restart to complete it. The swap is
therefore atomic and recoverable even if the process dies mid-restore.
Consequences
- Operators update, roll back, back up and restore a VayuPress site entirely from the browser, with no shell access and no manual restart.
- The self-update threat model is unchanged from ADR-0064: signatures are still mandatory, the opt-in env gate still applies, and unsafe modes still refuse.
- Self-restart relies on
execve(2)and is Linux-only, matching VayuPress's supported deployment target (Dockerfile /deploy/). On any re-exec failure the process exits for a supervisor to restart it, so there is no stuck state. - A restore replaces all content and settings; the pre-restore safety backup in the update-backups directory is the recovery path if a wrong file is imported.
- No new third-party dependencies: snapshots use only
archive/tar,compress/gzip,database/sql, andcrypto/sha256.
Revision — one-click apply gating (post-release)
The first cut reused the CLI's strict PreflightApply for the web button, which
requires both VAYU_SELFUPDATE_ENABLED=true and a pinned VAYU_RELEASE_PUBKEY.
In practice this left the Update now button disabled on a default install:
operators could see an available release but not install it, defeating the
one-click goal.
Decision: the authenticated admin UI now gates apply with PreflightMode (mode
check only) instead of the full env opt-in. An admin's explicit, role-checked
click is the opt-in. Verification is unchanged in spirit but tiered:
- The SHA-256 checksum is always verified (
ApplyVerifiedrequires the.sha256asset and a matching digest before any write). - The Ed25519 signature is still mandatory whenever a release key is pinned
(
VAYU_RELEASE_PUBKEY). With a key set, a missing or bad.sigfails the apply exactly as before. - When no key is pinned, the UI passes
ApplyOptions.AllowUnsigned=trueso the install proceeds on checksum verification alone. The strict CLI path leavesAllowUnsigned=false, sovayupress update applystill refuses an unsigned release and still requires the env opt-in — its threat model is unchanged.
Net effect: one-click updates work out of the box (checksum-verified, HTTPS fetch, automatic DB backup, atomic swap, self-restart, audit-logged), and pinning a release key strictly upgrades that to checksum+signature verification.