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
data/ directory.
Common tasks
Running the iPhone import helper
The companion Python tool inscripts/ios-app-import/ is stdlib-only and unrelated to the Node app — it produces a text file the web onboarding accepts.
Working with the SQLite database
Some patterns to know before you touchlib/db.ts or any helper that writes to the DB:
lib/db.tsexports a singletonbetter-sqlite3instance. 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(() => { … })()(seesaveToDbinlib/scraper.tsfor the canonical pattern). - Schema changes need to be applied in two places in
lib/db.ts: theCREATE TABLE IF NOT EXISTSblock (for fresh installs) and the inlinemigrationsarray ofALTER TABLEstatements (for existing installs). Forgetting one breaks upgrade paths. - Capture the previous snapshot before calling
saveToDb— it wipes and re-insertsprivacy_typesfor that app, and the diff loses every row if you capture after.
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.Adding a new locale
Editing copy goes through Crowdin (no JSON-mangling required). See Translations for the full workflow. The short version:- Add the language in Crowdin → Project Settings → Target Languages.
- Add the row to
crowdin.yml’slanguages_mapping.two_letters_codeif Crowdin’s ID doesn’t match your bundle filename. - Add the locale to next-intl’s supported list.
- Drop a placeholder
locales/<new>.jsoncontaining{}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.