Local setup, the commands you'll use, and the handful of things that trip people up.
Node 24 (current LTS) and npm.
npm install
cp .env.example .env # Google OAuth credentials + cookie secret
cp data/config.example.json data/config.json # calendars, location, PIN
npm run dev # http://localhost:3000.env needs a Google client ID and secret (docs/google-oauth-setup.md) and a COOKIE_SECRET (openssl rand -hex 32). TODOIST_API_KEY is optional; leave it blank and to-dos are off. Every key in both files is described in docs/configuration.md.
Enter your PIN at the login screen, then go to /setup to connect Google. The first calendar sync runs on boot and every 5 minutes after.
To skip the PIN while developing, run DEV_AUTH_BYPASS=1 npm run dev. Production builds ignore it.
The example config also ships a personal board. Open /b/kida in a 1280×800 viewport to see it (that's the real panel size). Its PIN is under boards.kida.pin, and the household PIN opens it too.
data/config.json is cached in memory for 60 seconds. If an edit seems to be ignored, that's why; restart the dev server rather than debugging the wrong thing.
npm run dev # dev server with hot reload
npm run build # production build
npm run start # serve the production build
npm test # Vitest, one pass
npm run test:watch # Vitest in watch mode
npm run lint # ESLint
npm run format # Prettier, write
npm run format:check # Prettier, check only
npm run weather-icons # regenerate lib/weather/weather-icon-svgs.ts from Iconify
npm run emoji-font # regenerate public/fonts/emoji/ and styles/fonts.cssBefore committing: npm test, npm run lint, npm run format:check, and npm run build all clean. CI runs the same four.
Vitest, deliberately light: the pure logic (layout packing, event linking, date math, filter, board scoping, to-do sorting), the DB layer, the auth pieces, config validation, the Todoist sync, and the write routes with Google and Todoist mocked. UI components aren't rendered in tests; layout changes are verified against a real browser instead (see docs/calendar.md).
Tests never open data/homehq.db. getDb() refuses the default path under Vitest, so a test that needs a database opens a temp file and calls _setDefaultDb(). Keep it that way; a fixture once wiped a live OAuth token.
The target is a 27" 4K panel at device scale factor 2, so a 1920×1080 browser viewport is the real layout. Everything is sized in rem off a root clamp() in styles/base.css; keep new CSS in rem (or em inside month view) so it scales with the wall.
To see the app on the actual display before the server exists, point a Pi's Chromium at your machine's dev server. next dev prints a Network: URL; add the Pi's hostname or IP to HOMEHQ_DEV_ORIGINS in .env so hot-reload assets are allowed through. docs/deployment.md has the details.
Everything in data/ except the example config is gitignored.
| File | Created by | Notes |
|---|---|---|
data/config.json |
you | Copy from the example |
data/config.example.json |
committed | The template; edit your copy, not this |
data/homehq.db |
app at runtime | SQLite, WAL mode, migrated on first run |
data/deploy-version |
deploy script | Build token the kiosk polls (see architecture) |
data/icons/ |
you, if needed | SVGs for local: title icons, for glyphs Font Awesome lacks |
Config errors on startup. The app validates config.json on load and the error names the field. Common ones: file missing (copy the example), PIN not a six-digit string, display.weatherIcons not one of the four sets, display.timezone not a valid IANA zone, and a display.titleIcons rule naming an icon the build doesn't have (the error lists the nearest matches). Boards add a few more: a slug with characters other than lowercase letters, digits and dashes, a board naming a calendar id that isn't in the top-level list, ownCalendars, alwaysShow or defaultCalendar naming a calendar the board doesn't show, and two boards claiming the same host. In production the example PIN 123456 is refused outright, on the household and on any board.
next build fails with useContext or <Html> errors. NODE_ENV=development is set in your shell profile. Next sets it itself; remove yours (echo $NODE_ENV to check) and open a new terminal.
A long-running dev server keeps overwriting data. Restart npm run dev after changing anything the background syncs touch (lib/google/sync.ts, lib/todoist/sync.ts, lib/weather/sync.ts, normalizeEvent, the DB write helpers). The sync schedulers start once, from instrumentation.ts, and Next's hot reload never re-instantiates that module graph. So a server left running overnight keeps executing yesterday's sync code against today's database every 5 minutes. It looks like a product bug: writes through the (hot-reloaded, current) API routes land correctly, then the next sync quietly reverts them. The tell is a row whose updated_at matches sync_status.last_success and a new column that's empty.
Sync indicator says "Sync failing". If it adds "reconnect Google at /setup", the refresh token is gone: revoked, or (for an External Google app still in Testing) expired after seven days (why, and the fix). Otherwise the full error is in the server log ([sync] lines in the dev console, or journalctl -u homehq in production).
Emoji render as boxes on the Pi. They shouldn't; the app ships its own emoji font. If they do, check that styles/fonts.css lists Inter before the emoji face and that public/fonts/emoji/ has all ten slices. npm test covers both.