This page is for running privacytracker out of a source checkout — for development work, custom builds, or platforms where prebuilt binaries aren’t published yet (Windows, Linux desktop). If you just want to use privacytracker, the self-host quickstart is faster.

Prerequisites

1

Install Node 24 LTS

privacytracker pins to Node 24 in engines.node (>=24.0.0 <27.0.0), and .nvmrc is 24. Earlier versions will fail npm install on the better-sqlite3 native build.
2

Install build essentials (Linux only)

better-sqlite3 builds a native module on first install. On Debian/Ubuntu:
macOS gets these automatically via the Xcode Command Line Tools (xcode-select --install).

Clone and run

npm run dev boots Next.js in dev mode with hot reload. The SQLite database is created on first request at ./data/privacy.db.

Production build

This produces the same artefact that ships in the desktop app and Docker image. The output is a single Next.js standalone bundle plus the data/ directory.

Common tasks

Running the iPhone import helper

The companion Python tool in scripts/ios-app-import/ is stdlib-only and unrelated to the Node app — it produces a text file the web onboarding accepts.
Run its tests with:

Working with the SQLite database

Some patterns to know before you touch lib/db.ts or any helper that writes to the DB:
  • lib/db.ts exports a singleton better-sqlite3 instance. Don’t open a second one.
  • Pragmas set on open: journal_mode = WAL, busy_timeout = 5000, foreign_keys = ON.
  • All DB calls are synchronous. Multi-step writes use db.transaction(() => { … })() (see saveToDb in lib/scraper.ts for the canonical pattern).
  • Schema changes need to be applied in two places in lib/db.ts: the CREATE TABLE IF NOT EXISTS block (for fresh installs) and the inline migrations array of ALTER TABLE statements (for existing installs). Forgetting one breaks upgrade paths.
  • Capture the previous snapshot before calling saveToDb — it wipes and re-inserts privacy_types for that app, and the diff loses every row if you capture after.
The full reasoning is in Architecture.

Adding a new feature flag

Three edits, in this order:
1

Add the key to the union

lib/feature-flag-rules.ts → add it to the FlagKey union. Typos fail at tsc.
2

Add a HARD_DEFAULTS entry

Every key needs a hard default — that’s what the kill-switch falls back to.
3

Add rules only if behaviour differs

Populate AUDIENCE_RULES, GOAL_RULES, or ACCESSIBILITY_RULES only when the value should differ from the hard default. Most flags don’t care about most inputs — keep the rule tables sparse.
Full design rationale: Feature flags.

Adding a new locale

Editing copy goes through Crowdin (no JSON-mangling required). See Translations for the full workflow. The short version:
  1. Add the language in Crowdin → Project Settings → Target Languages.
  2. Add the row to crowdin.yml’s languages_mapping.two_letters_code if Crowdin’s ID doesn’t match your bundle filename.
  3. Add the locale to next-intl’s supported list.
  4. Drop a placeholder locales/<new>.json containing {} so the first build succeeds.

Where the code lives

The dense engineering reference (every gotcha, every Apple-HTML quirk, the apostrophe variants in the privacy-policy aria-label, the Array.isArray(raw) ? raw : raw.data unwrap) lives in AGENTS.md (for Codex) and CLAUDE.md (for Claude Code) at the repo root. Coding agents read those directly. Humans should too.