-
Notifications
You must be signed in to change notification settings - Fork 0
Development Guide
Stored as integer pence (smallest GBP unit) in the DB. Always format on render, never store formatted strings.
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.
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.
Use the @/* alias (configured in tsconfig.json). No relative ../../ imports.
Only add a comment when the WHY is non-obvious. Code should be self-documenting.
Default en-GB, Europe/London, GBP throughout. Don't introduce other locales or currencies without updating docs/DECISIONS.md.
| 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. |
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.
Session strategy must be JWT. The Credentials provider with database sessions is explicitly unsupported by Auth.js. Do not switch back.
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.
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.
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.
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.
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.
The local storage strategy writes to public/uploads/ inside the container. Without a Docker volume mount, uploads are lost on container restart.
- Edit
db/schema.ts. - Run
npm run db:push(native) or restart withdocker compose up --build(Docker — themigrateservice runsdrizzle-kit pushon startup). - No migration files are generated —
db:pushdirectly syncs the schema. This is intentional for a personal app with a single DB.
| 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. |