instrumentation.ts on startup, so almost every upgrade is just get the new binary or image, restart, done.
This page covers the version-agnostic process. For per-release notes — what specifically changed, breaking changes if any — see the Changelog.
The general flow
1
Take a backup before the upgrade
A reflex worth practising even when migrations are reliable. The simplest version:Or use Settings → Backup → Export bundle for a versioned JSON copy. See Backup & restore for the bundle format.
2
Pull the new release
Pick the path that matches your install:
3
Watch boot logs for the migration line
instrumentation.ts writes one activity row per migration step. The aggregate row at the end looks like:4
Verify health
What runs on boot
instrumentation.ts runs serially before the app accepts requests:
- Schema migrations. Every
CREATE TABLE IF NOT EXISTSruns (no-op for existing tables) plus the inlinemigrationsarray ofALTER TABLEstatements. Each migration is idempotent — running it twice is safe. - Feature-flag migration — six ordered, idempotent steps ending with the focus-goal rename. See Feature flags → Migration.
- Bulk-runner state checks (3 staggered: 8s, 10s, 12s after boot). For each of
sync_running,wayback_import_running,policy_sync_running: no-op, heal a stale lock, or auto-resume from a saved state blob withinitiator: 'resume'. - Quarantine sweep. Any
feature_flag_overridesrow whose key isn’t in this build’sFlagKeyunion getsquarantined = 1; rows previously quarantined whose keys are back get cleared. This is what makes downgrades-then-upgrades not lose your overrides.
Reading the activity log
The activity log is the canonical record of what happened during an upgrade. Useful filters when investigating: Rows live inactivity_log, with the category in type and the detail in summary. Useful slices when investigating:
Stale-mutex heals and resumes also raise a bell notification — those carry
sync_stale_cleared / sync_resumed and their wayback and policy equivalents in the notifications table, not here.
The log is capped at the most recent 2,000 events, pruned inside recordActivity on every write (ACTIVITY_RETENTION in lib/activity.ts). No route deletes an individual entry, but the whole table does get wiped: activity_log is in APP_DATA_TABLES_TO_TRUNCATE (lib/reset-tables.ts), so both POST /api/admin/start-over and POST /api/dev/wipe-apps issue DELETE FROM activity_log.
If a migration fails
The boot sequence renders an error screen with the failing step name, an attempt counter, and a Try again button. Most failures are transient — file locks, slow disks — and clear on the second attempt.1
Read the step name
The error message looks like Migration step
notification_prefs_absorb failed: …. Steps are named so you can grep the codebase:2
Tap Try again
Migrations are idempotent. Up to 3 retries are offered.
3
If retries exhaust, take a backup of the broken DB
The error screen offers a Reset DB escape hatch. Before you touch it:
4
Reset DB or restore from backup
Reset DB wipes
data/privacy.db and routes you to onboarding. You’ll re-import apps from your earlier backup via Settings → Backup → Import bundle, which exercises the same migrations on a fresh DB.If the bundle import also fails, the bundle is older than the running release expects — try a newer bundle, or downgrade privacytracker to a version that accepts the bundle.5
Open an issue
Migration failures are bugs we want to fix. Paste the broken DB schema (
sqlite3 privacy.db.broken-* ".schema") plus the activity log slice covering the failure into a GitHub issue.Rolling back
privacytracker supports forward migrations only. There’s no built-in downgrade path because:- Schema migrations are non-reversible without losing data added by the new version.
- The bundle format is forward-compatible — an older bundle format restores into a newer runtime — but not backward-compatible. A bundle whose integer
versionexceeds what the running release supports is refused outright, with a message telling you to upgrade.
- Stop the new version.
- Replace
data/privacy.dbwith the pre-upgrade copy you took in step 1 of The general flow. (You did take one, right?) - Reinstall the older version (Homebrew:
brew install --cask privacytracker@<version>if a pinned cask exists, otherwise download the older.dmg; Docker:docker compose pull <older-tag> && up).
Rolling back across the release that moves the desktop app from its Node sidecar to a Rust backend works the same way. Both builds keep the database in the same data folder and the same format, and either opens what the other left behind, so the switch itself never stands in the way of going back to a release on Node. Tauri & the backend covers the move.
Per-release notes
The Changelog mirrorsCHANGELOG.md from the main repo. As releases land, this page should grow a per-version section here too — covering anything that isn’t automatic and what to manually verify after upgrading.
The template for a per-release entry looks like: