Skip to content

Development Guide

dbwg2009 edited this page May 4, 2026 · 1 revision

Development Guide

Conventions

Money

Stored as integer pence (smallest GBP unit) in the DB. Always format on render, never store formatted strings.

Dates

Stored as ISO yyyy-mm-dd (date column). For birthdays, birth_year_known: false means the year is a placeholder — don't show the year in the UI when this is false.

Mutations

All writes go through Drizzle ORM in server actions (app/people/actions.ts). No raw SQL unless absolutely necessary. No REST routes for mutations — only /api/auth/* and cron use route handlers.

Imports

Use the @/* alias (configured in tsconfig.json). No relative ../../ imports.

Comments

Only add a comment when the WHY is non-obvious. Code should be self-documenting.

Currency / locale

Default en-GB, Europe/London, GBP throughout. Don't introduce other locales or currencies without updating docs/DECISIONS.md.


File responsibilities

File What it owns
db/schema.ts Source of truth for DB shape. All table/column changes go here first.
lib/auth.ts Auth.js v5 config (Credentials provider, JWT sessions).
lib/people-queries.ts Read queries only (listPeopleSummary, getPersonDetail, requireCurrentUserId).
app/people/actions.ts All write server actions — person CRUD, wishlist CRUD, product actions. Shared between list and detail pages.
lib/products/search.ts Product search orchestrator — calls OpenRouter, falls back to eBay, returns unified results.
lib/reminders.ts Reminder scheduling, digest builder, runDailyReminders (called by cron endpoint).
lib/storage.ts Photo upload handler. Reads STORAGE_STRATEGY to decide local vs base64.

Known gotchas

Standalone Docker image

The runner Docker stage is a Next.js standalone image. It does not have drizzle-kit or devDeps. Migrations run via the separate migrate service in compose, which uses the migrator target from the same Dockerfile.

Auth.js + Credentials provider

Session strategy must be JWT. The Credentials provider with database sessions is explicitly unsupported by Auth.js. Do not switch back.

OpenRouter free models

Rate-limited to a few requests per minute per account. Two sources of 429s:

  • Per-account caps (your quota)
  • Upstream provider limits (affects all users — shows as Provider returned error)

The search orchestrator catches both and falls through to eBay. Switch model via OPENROUTER_MODEL if rate-limited consistently.

LLM-generated URLs

OpenRouter models have no web access. Product URLs returned by the LLM may be hallucinated. The UI shows an "AI" badge on AI-sourced saved products. eBay-sourced and manual products are the trusted source for real links.

Reminder idempotency

runDailyReminders() is safe to call multiple times. last_sent_for_year on each Reminder row ensures the same reminder won't fire twice in a cycle. Triggering the cron endpoint manually during development is safe.

postgres-js connection pool

idle_timeout is set to 600s in db/index.ts to prevent pool thrashing on a low-traffic Raspberry Pi. If switching to Neon, add ?sslmode=require to DATABASE_URL.

iCal token

users.ical_token is a UUID auto-generated on account creation. It must be non-null for the feed route to work. Resetting it (via Settings) invalidates all existing calendar subscriptions.

Photo uploads persistence

The local storage strategy writes to public/uploads/ inside the container. Without a Docker volume mount, uploads are lost on container restart.


Making schema changes

  1. Edit db/schema.ts.
  2. Run npm run db:push (native) or restart with docker compose up --build (Docker — the migrate service runs drizzle-kit push on startup).
  3. No migration files are generated — db:push directly syncs the schema. This is intentional for a personal app with a single DB.

Locked decisions — don't change without updating docs/DECISIONS.md

Area Decision
LLM OpenRouter only. Not Claude, OpenAI direct, or Gemini.
Product search OpenRouter (primary) + eBay Browse API (fallback). No SerpAPI, no scraping.
Reminders Resend email only.
DB PostgreSQL via postgres-js.
Currency GBP / en-GB / Europe/London.
Auth JWT sessions, Credentials provider (email + password).
UI Bespoke Tailwind components. No shadcn or other component libraries without explicit approval.