Development Guide
For ongoing code health reviews, use the Maintainability Checklist. For ownership rules across the current workspace layout, use Architecture Boundaries. For docs ownership, navigation, and issue hygiene, use Documentation Governance.
Prerequisites
- Node.js 24+ and npm (match the version required by the
package.jsonenginesfield) - Git
- VS Code (recommended)
Development Scripts
Testing
npm run test- Run all tests using Vitestnpm run test:server- Run utility function tests (Node.js environment)npm run test:services- Run shared package and service testsnpm run test:storybook- Run Storybook component tests (browser environment)
Code Quality
npm run lintornpm run lint:check- Run ESLint for code lintingnpm run lint:fix- Fix ESLint issues automaticallynpm run format:check- Check code formatting with Prettiernpm run format:write- Fix code formatting with Prettiernpm run format:package- Sort and format package.jsonnpm run type-check- Run TypeScript type checkingnpm run fix- Run all fixes (lint, format, package.json)npm run check:backoffice- Build shared code, run the backoffice structure guard, type-checkapps/backoffice, and run the backoffice Vitest suitenpm run quality:backoffice- Run the backoffice structural module-size guard used to keep operator UI/routes split into reviewable files$impeccable critique <target>- Run before publishing UI PRs, then address actionable findings or document deferred follow-ups in the PR. Raw.impeccable/critique/*snapshots are local working notes and should not be committed. Move durable UI decisions intoDESIGN.md,docs/UI-DEVELOPMENT.md, roadmap docs, issues, or the PR description.
Development
npm run dev- Start development servernpm run dev:docs- Start the Fumadocs documentation site onhttp://localhost:3003npm run dev:backoffice- Start the operator backoffice UI onhttp://localhost:3004by default; override withPORT=4030 npm run dev:backofficenpm run build- Build production applicationnpm run build:docs- Build the Fumadocs documentation sitenpm run build:backoffice- Build the operator backoffice appnpm run start --workspace=apps/web- Start production servernpm run start --workspace=apps/docs- Start the production documentation server afternpm run build:docsnpm run copy:env- Copy the root.envintoapps/*,services/*, andpackages/sharednpm run migrate:db- Apply all idempotent SQL migrations fromdb/initnpm run storybook- Start Storybook development servernpm run build:storybook- Build Storybook for production
Workspace-local scripts used during app development:
npm run start:workers --workspace=apps/web- Start BullMQ workers for async recommendationsnpm run bull-board- Open the BullMQ dashboard locally (OPERATOR_AUTH_USERNAMEandOPERATOR_AUTH_PASSWORDenable the same login used in production)npm run catalog:discovery:enqueue- Queue TMDB discovery pages for the rate-limited catalog workernpm run catalog:backfill:enqueue- Queue existing movies for the rate-limited catalog backfill worker
Database & Data Management
npm run setup:local-db- Start local PostgreSQL + Redis via Docker and write credentials to.envnpm run cleanup:local-db- Stop containers and remove the database volume (full reset)npm run analyze-movies --workspace=apps/web- Analyze movie data chunks for embedding optimizationnpm run calibrate-similarity --workspace=apps/web- Recalibrate recommendation similarity thresholds against the live DB
Database Setup Workflow
The project includes scripts to help you set up and manage your movie recommendation database. All commands run from the repo root:
- Start the DB - Run
npm run setup:local-dbto spin up PostgreSQL + Redis and initialize credentials - Sync env files - Run
npm run copy:envso the web app and services use the updated root.env - Run the app and workers - Start
npm run devfrom the repo root andnpm run start:workers --workspace=apps/webin a second terminal - Seed the DB - Start
npm run dev:backoffice, open the Catalog seed page, and trigger the curated seed job - Reset - Run
npm run cleanup:local-dbto wipe everything, then repeat from step 1
The curated seed worker reads its movie list from apps/web/data/movies.txt.
Schema and Metadata Notes
Features that depend on movie metadata need both schema and data to be present. For example, movie-memory cards use poster URLs, localized names, TMDB ids, and movie identity keys. When adding fields like these:
- add the column to the schema initialization path
- add an idempotent migration or
ALTER TABLE ... ADD COLUMN IF NOT EXISTSpath for existing local, preview, and production databases - update seed/backfill code so existing rows eventually receive the new data
- make the UI degrade gracefully while the backfill is incomplete
- document whether an existing Coolify preview volume needs to be recreated
If a preview deployment has a long-lived PostgreSQL volume, new code may run against an older schema until migrations run or the preview is recreated.
Local Runbook
For seeded local data that matches the async recommendation flow, use this sequence from the repo root:
cp .env.example .env
npm install
npm run setup:backoffice:local-datasetup:backoffice:local-data runs setup:local-db, copy:env, and
leaves catalog seeding to the Backoffice Catalog seed action. Use those
individual scripts only when you need to inspect or rerun one step.
Then run the app in separate terminals:
# terminal 1
npm run dev
# terminal 2
npm run start:workers --workspace=apps/web
# optional terminal 3
npm run bull-board
# terminal 4, when you need curated catalog rows
npm run dev:backofficeIf you change the root .env, re-run npm run copy:env before restarting the app or workers.
Local Backoffice Fixture Mode
For backoffice UI development, prefer the deterministic local fixture path when you do not need the full seeded catalog or live TMDB/OpenAI credentials:
npm run setup:backoffice:fixtures
npm run dev:backoffice:fixturessetup:backoffice:fixtures starts the isolated e2e PostgreSQL and Redis services
on 127.0.0.1:55432 and 127.0.0.1:56379, applies migrations, and seeds a
small deterministic catalog. dev:backoffice:fixtures starts the Next.js
backoffice on http://127.0.0.1:3004 against those fixtures with operator auth
disabled. This is the fastest path for catalog-health, TMDB-review,
repair-batch, and manual-metadata UI work. Use the seeded local data runbook
above when you need real seed/discovery data or app/worker integration.
Next.js allows only one dev server per app directory. If you already have
npm run dev:backoffice running, stop that process before starting
npm run dev:backoffice:fixtures; otherwise Next.js will report that another
dev server is already running.
Mock Data Fallback
When DATABASE_URL is not set (or the database client is not configured), the /api/movies route automatically returns generated mock data instead of querying PostgreSQL. This means the Available Movies page works out of the box in local development without any database setup — you will see placeholder movies with realistic fields (name, age rating, duration, score, year).
To switch to real data, add DATABASE_URL to your .env file and seed the database as described above.
Code Style and Conventions
- TypeScript - All new code should be TypeScript
- ESLint - Follows Next.js and Prettier configurations
- Prettier - Code formatting is enforced
- Import organization - Auto-sorted with eslint-plugin-import
Testing Strategy
- Unit tests - Vitest for utility functions and business logic
- Component tests - Storybook with Vitest integration
- Browser tests - Playwright for end-to-end scenarios
End-to-End Database Harness
Run the deterministic e2e smoke suite from the repo root:
npm run test:e2eThe command prepares an isolated environment:
docker-compose.e2e.ymlstarts PostgreSQL with pgvector on127.0.0.1:55432and Redis on127.0.0.1:56379.scripts/e2e/setup-db.mjsresets the e2e compose project, applies everydb/init/*.sqlmigration throughapps/web/scripts/migrate-db.js, and seeds deterministic real-movie fixtures.apps/web/playwright.e2e.config.tsstarts the app onhttp://127.0.0.1:3100withDATABASE_URLandREDIS_URLpointed at the isolated services.apps/backoffice/playwright.e2e.config.tsstarts the operator app onhttp://127.0.0.1:3101against the same isolated services after the web e2e suite completes.E2E_DETERMINISTIC_RECOMMENDATIONS=1makes quiz submissions complete from seeded fixtures instead of calling live OpenAI/TMDB services or requiring a worker process.
Keep the default e2e seed curated and deterministic. A sanitized dev dump can be useful for one-off local catalog investigations, but it should not become the CI/default e2e source unless users, sessions, recommendation history, feedback, secrets, and operational records are stripped first.
To run only the backoffice browser smoke suite while still preparing the isolated database and Redis services:
npm run test:e2e:backofficeFor local backoffice PR validation before browser e2e, run:
npm run check:backofficeStop and remove the e2e services with:
npm run test:e2e:downThe smoke coverage proves /api/health, Available Movies filtering and empty states, auth/session flows, solo quiz submission, result rendering, recommendation feedback, movie-memory persistence, backoffice catalog repair enqueue/audit visibility, and the TMDB review decision flow. AI-response evals remain separate so they can add fixture scoring and optional live-model runs without slowing normal e2e.
Recommendation Eval Harness
Run the deterministic recommendation eval suite from the repo root:
npm run eval:recommendationsThe default eval path uses fixture prompts, fixture user-memory constraints, and mocked recommendation outputs. It does not call OpenAI, TMDB, Redis, or PostgreSQL. The runner writes apps/web/test-results/recommendation-evals/report.json and prints a concise pass/fail summary. Current scoring checks:
- response shape against the
ApiResponseschema - main pick and alternates stay inside the fixture candidate set
- forbidden safety terms are absent
- watched, rejected, and explicitly forbidden titles are not repeated
- explanation text is specific enough for the fixture expectations
- 0.2.0 taste-control signals remain visible and enforceable: explicit hard avoids, runtime constraints, discovery appetite, optional reference movie flows, and feedback-derived memory
Run live OpenAI evals only when intentionally checking model/provider behavior:
npm run eval:recommendations -- --liveLive evals require configured provider and database environment variables such as OPENAI_API_KEY, DATABASE_URL, and usually TMDB_API_KEY. They call the provider-backed recommendation pipeline and persist the provider response in the eval report. CI blocks on the deterministic eval job; live evals are manual so normal PR checks stay cheap and predictable.
Run real-data evals when checking catalog retrieval, seed/backfill, schema, or candidate-availability changes:
npm run test:e2e:setup
DATABASE_URL=postgresql://popchoice_e2e@127.0.0.1:55432/popchoice_e2e npm run eval:recommendations:real-data
npm run test:e2e:downThe real-data mode still uses controlled recommendation outputs and does not call live AI providers. It adds catalog connectivity, candidate availability, and catalog search retrieval checks to the normal eval report.
Use three eval levels when changing AI-related code:
npm run eval:recommendationsfor every PR that changes recommendation prompts, embeddings, candidate filters, ranking, feedback signals, response shape, or eval fixtures.- A real-data eval for changes that affect catalog retrieval, schema, seeding/backfill, or candidate availability. This runs against a seeded database through
npm run eval:recommendations:real-dataor the scheduled/manual GitHub workflow; it is not a default per-PR hard gate. npm run eval:recommendations -- --liveonly when intentionally checking live model/provider behavior before larger recommendation changes. Live runs can spend API credits and depend on provider availability, so agents should call out whether they ran it or why they did not.
Project Structure
apps/
├── docs/ # Fumadocs documentation site rendering docs/
├── web/
│ └── src/
│ ├── app/ # Next.js route and page boundaries
│ ├── clients/ # Low-level infrastructure clients
│ ├── components/ # Reusable React components
│ ├── features/ # Feature-owned orchestration and domain-facing modules
│ ├── hooks/ # React hooks
│ ├── i18n/ # Locale support
│ ├── integrations/ # App-local third-party API wrappers
│ ├── lib/ # App-local infra helpers and adapters
│ ├── mocks/ # Mock handlers and test support
│ ├── styles/ # Styling and theme assets
│ └── utils/ # Reusable utility helpers
├── bull-board/ # Queue monitoring app
├── backoffice/ # Operator app for catalog health and review
packages/
├── shared/ # Shared database, embedding, logging, and utility code
└── ui/ # Shared shadcn-derived UI primitives
services/
├── movie-discovery/ # Scheduled or one-shot TMDB discovery process
├── movie-backfill/ # Manual TMDB metadata maintenance CLI / fallback
└── db-migrate/ # Containerized database migration runtime
db/ # SQL scripts and DB initialization assetsBoundary Notes
apps/web/src/appshould stay focused on route/page boundaries.apps/web/src/featuresshould own cross-route product behavior such as recommendation, auth, and catalog workflows.apps/web/src/clientsshould own low-level client setup.apps/web/src/integrationsshould own app-local wrappers around third-party APIs.apps/web/src/libshould hold app-local infrastructure helpers, not become a catch-all business layer.apps/web/src/utilsshould stay narrow and reusable, not absorb end-to-end orchestration.- Root
services/*are standalone background or offline runtimes.
See BOUNDARIES.md for the full ownership definitions.