Motivation
The startup revision guard (#50) makes upgrades explicitly one-way: once a binary migrates a database, an older binary refuses to serve it. For v1.4.0-beta.2 the release notes had to tell users "copy opal.db somewhere first if you might want to roll back." That should be something OPAL does for them — it owns the database and knows exactly when a migration is about to change it.
This is the standard pattern for local-first apps that own their store (Home Assistant, the *arr family): snapshot immediately before applying migrations, so rollback = old binary + restored file.
Design
- Trigger: in
init_database(), only when the stamped revision differs from head — i.e. only when migrations will actually run. Normal starts are untouched; one backup per upgrade, not per launch.
- Mechanism:
VACUUM INTO (or the sqlite3 backup API), not a file copy — atomic, consistent, and avoids the WAL/-shm sidecar problem. Compacts as a bonus.
- Naming:
backups/opal-pre-<revision>-<ISO8601>.db in the data dir. Keying on the pre-upgrade revision means a failed upgrade retried on next launch won't clobber the good snapshot with a half-migrated one, and the restore procedure is self-describing.
- Retention: prune to the last N,
OPAL_UPGRADE_BACKUPS (default 1). Databases are megabytes; cost is negligible.
- Skip: demo databases (
demo.<name> is throwaway by design) and in-memory engines.
- Discoverability: the migration log line and the revision-guard error message both point at the backup path, so the rollback story surfaces exactly when someone needs it.
Non-goals
- Protecting against migrations that succeed but were wrong and noticed days later — that's general backup strategy, not this feature.
- An
opal restore command — maybe later; for now the restore procedure is "replace the file", documented in the log line.
🤖 Generated with Claude Code
Motivation
The startup revision guard (#50) makes upgrades explicitly one-way: once a binary migrates a database, an older binary refuses to serve it. For v1.4.0-beta.2 the release notes had to tell users "copy
opal.dbsomewhere first if you might want to roll back." That should be something OPAL does for them — it owns the database and knows exactly when a migration is about to change it.This is the standard pattern for local-first apps that own their store (Home Assistant, the *arr family): snapshot immediately before applying migrations, so rollback = old binary + restored file.
Design
init_database(), only when the stamped revision differs from head — i.e. only when migrations will actually run. Normal starts are untouched; one backup per upgrade, not per launch.VACUUM INTO(or the sqlite3 backup API), not a file copy — atomic, consistent, and avoids the WAL/-shmsidecar problem. Compacts as a bonus.backups/opal-pre-<revision>-<ISO8601>.dbin the data dir. Keying on the pre-upgrade revision means a failed upgrade retried on next launch won't clobber the good snapshot with a half-migrated one, and the restore procedure is self-describing.OPAL_UPGRADE_BACKUPS(default 1). Databases are megabytes; cost is negligible.demo.<name>is throwaway by design) and in-memory engines.Non-goals
opal restorecommand — maybe later; for now the restore procedure is "replace the file", documented in the log line.🤖 Generated with Claude Code