privacytracker takes a forward-only, automatic-on-boot approach to upgrades. New releases bring schema migrations that run idempotently in 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:
From the UI: Settings → Admin → Developer Options → Activity log, filtered to Migration. From the CLI:
If the aggregate row says N/N steps, you’re done.
4

Verify health

Click through a few app detail pages, the dashboard, the bell. If anything looks wrong, see If a migration fails below.
That’s the happy path. It’s been the entire process for every upgrade so far.

What runs on boot

instrumentation.ts runs serially before the app accepts requests:
  1. Schema migrations. Every CREATE TABLE IF NOT EXISTS runs (no-op for existing tables) plus the inline migrations array of ALTER TABLE statements. Each migration is idempotent — running it twice is safe.
  2. Feature-flag migration — six ordered, idempotent steps ending with the focus-goal rename. See Feature flags → Migration.
  3. 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 with initiator: 'resume'.
  4. Quarantine sweep. Any feature_flag_overrides row whose key isn’t in this build’s FlagKey union gets quarantined = 1; rows previously quarantined whose keys are back get cleared. This is what makes downgrades-then-upgrades not lose your overrides.
None of these block the UI. If migrations fail, the boot sequence renders an error screen instead of the normal app — see If a migration fails.

Reading the activity log

The activity log is the canonical record of what happened during an upgrade. Useful filters when investigating: Rows live in activity_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 version exceeds what the running release supports is refused outright, with a message telling you to upgrade.
If you need to roll back:
  1. Stop the new version.
  2. Replace data/privacy.db with the pre-upgrade copy you took in step 1 of The general flow. (You did take one, right?)
  3. 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).
Any data you wrote between the upgrade and the rollback is lost.
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 mirrors CHANGELOG.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:
For now, all upgrades follow the standard flow above and have no version-specific manual steps.