Setup Guide
Environment Variables
Create a .env file in your project root with the following variables:
# OpenAI Configuration
OPENAI_API_KEY=your-openai-api-key-here
# Database – PostgreSQL connection string
DATABASE_URL=postgresql://user:password@host:5432/dbname
# Local Docker PostgreSQL credentials (set automatically by npm run setup:local-db)
POSTGRES_USER=postgres
POSTGRES_PASSWORD=your-generated-password
POSTGRES_DB=popchoice
# TMDB API (for movie data)
TMDB_API_KEY=your-tmdb-api-key
# API key authentication secret (used to derive scrypt key digests)
# Required when VALID_API_KEYS is set; must stay stable across restarts
API_KEY_HMAC_SECRET=your-stable-hmac-secret
# Session signing secret for login/logout cookies
# Optional in development (falls back to API_KEY_HMAC_SECRET, then an ephemeral dev secret)
# Required in production if users can log in; must stay stable across restarts
AUTH_SESSION_SECRET=your-stable-session-secret
# Redis (optional) – enables distributed rate limiting and BullMQ background workers
# When unset, rate limiting is skipped and background seeding is disabled
REDIS_URL=redis://user:password@host:6379
# API key authentication – comma-separated scrypt digests of valid API keys
# Required in production; when unset in development, auth is disabled with a warning
VALID_API_KEYS=<scrypt-digest-of-key1>,<scrypt-digest-of-key2>
# Public base URL of the app (required behind a reverse proxy such as Coolify)
# Used for same-origin CSRF validation; without this, browser API calls will return 401
NEXT_PUBLIC_BASE_URL=https://your-domain.example
# Password reset email delivery (required in production for forgot-password flow)
RESEND_API_KEY=your-resend-api-key
EMAIL_FROM=PopChoice <noreply@mail.your-domain.example>The root .env is the source of truth for local development. After you update it, run npm run copy:env from the repo root to sync the workspace-level .env files used by apps/web and the local services.
OpenAI Setup
This application uses the OpenAI API to generate embeddings and chat completions.
OpenAI Setup Steps
-
Get your OpenAI API key
- Sign up or log in at OpenAI Platform
- Go to your account settings and create an API key
-
Add your API key to the
.envfile- Add the line:
OPENAI_API_KEY=your-openai-api-key-here
- Add the line:
-
Verify setup
- Your application will automatically load the API key from
.env - See
apps/web/src/clients/openaiClient.tsfor app client setup
- Your application will automatically load the API key from
PostgreSQL Database Setup
This application uses a generic database client abstraction (apps/web/src/clients/dbClient.ts) backed by PostgreSQL via pgClient.ts for storing movie embeddings.
PostgreSQL Setup Steps
-
Provision a PostgreSQL database
-
Enable the pgvector extension Connect to your database and run:
CREATE EXTENSION IF NOT EXISTS vector; -
Set up the database schema
- Run the SQL from
db/createDB.sql
- Run the SQL from
-
Add the matching function
- Run the SQL from
db/match_movies.sql
- Run the SQL from
-
Configure environment variables Add
DATABASE_URLto your.envfile:DATABASE_URL=postgresql://user:password@host:5432/dbname -
Apply migrations and populate the database
- Run
npm run migrate:dbif you are using an existing database - Run
npm run populate-db
- Run
TMDB API Setup (Optional)
-
Create TMDB account
- Sign up at The Movie Database
- Request an API key from your account settings
-
Add to environment
- Add
TMDB_API_KEY=your-keyto your.envfile
- Add
API Endpoint Protection
The /api/movie-recommendation, /api/more-tmdb-picks, /api/movies, /api/recommendations, and /api/recommendations/[id]/feedback endpoints are protected by the withAuth wrapper and accept either:
- API key authentication (
Authorization: Bearer <key>orX-API-Key) - Same-origin browser requests with a valid CSRF cookie/header pair
1. API key authentication (recommended for external/service callers)
External callers (mobile apps, partner integrations) can authenticate via one of these headers:
Authorization: Bearer <key>X-API-Key: <key>
Keys are stored as scrypt digests in the VALID_API_KEYS environment variable (comma-separated). The derivation uses a server-side secret from API_KEY_HMAC_SECRET. Never store plaintext keys.
Generating and registering a key
-
Generate a random key (e.g. using
openssl rand -hex 32) -
Set
API_KEY_HMAC_SECRETin your environment (e.g.openssl rand -hex 32). This secret must stay the same across restarts. -
Hash the key using the utility exported from
apps/web/src/lib/apiAuth.ts, or generate both values with this command:API_KEY_HMAC_SECRET="$(openssl rand -hex 32)" node -e "const {randomBytes,scryptSync}=require('crypto'); const secret=process.env.API_KEY_HMAC_SECRET; const key=randomBytes(32).toString('hex'); const hash=scryptSync(key, secret, 32, {N:16384,r:8,p:1}).toString('hex'); console.log('API_KEY_HMAC_SECRET='+secret); console.log('PLAINTEXT_API_KEY='+key); console.log('VALID_API_KEYS='+hash)"Or from application code:
import { hashApiKey } from '@/lib/apiAuth'; console.log(hashApiKey('your-plaintext-key')); -
Add the hash to your environment:
API_KEY_HMAC_SECRET=<your-derivation-secret> VALID_API_KEYS=<scrypt-digest-of-key1>,<scrypt-digest-of-key2> -
Distribute the plaintext key to your API consumers — they send it in the header; store the digest in the server environment.
Development mode
When VALID_API_KEYS is not set, API key authentication is disabled in development (NODE_ENV !== 'production') and a warning is logged. In production, all API requests are rejected when the variable is absent.
2. CSRF tokens (browser fallback path)
Next.js proxy (apps/web/src/proxy.ts, the App Router successor to middleware) issues a random __csrf cookie on page loads. Client-side code reads the cookie and echoes it in the X-CSRF-Token request header on API calls.
The frontend reads the __csrf cookie and echoes it in X-CSRF-Token for API calls. CSRF fallback is accepted only for same-origin browser requests and only when both cookie and header are present and identical.
Signed-in account APIs such as /api/account and /api/account/movie-memory use the session cookie directly and still require the CSRF header for mutating browser requests.
Redis Setup (Optional – Rate Limiting & Background Workers)
Redis is used for two features:
- Rate limiting –
/api/movie-recommendationis limited to 10 requests per minute per IP. WhenREDIS_URLis absent, rate limiting is skipped and the API fails open. - BullMQ workers – recommendation, more-picks, and TMDB seeding jobs are queued via BullMQ. When
REDIS_URLis absent, workers are disabled and supported routes use inline or disabled fallbacks.
Security note:
REDIS_URLmay contain credentials (e.g.redis://user:password@host:6379). Store it as a secret in your deployment environment (e.g. Railway/Vercel secret, GitHub Actions secret) and never commit it to source control.
Local Redis with Docker
A redis service is included in docker-compose.yml. Start it with:
docker compose up redis -dThen add to your .env:
REDIS_URL=redis://localhost:6379If you are using the repo's local setup flow, run npm run copy:env after changing the root .env so apps/web/.env picks up the same REDIS_URL.
Run background workers in a separate terminal:
cd apps/web
npm run start:workersOptional queue monitoring dashboard:
cd apps/web
npm run bull-boardLocal Bull Board runs without a login unless OPERATOR_AUTH_USERNAME and
OPERATOR_AUTH_PASSWORD are set. Production deployments should always set
those credentials before assigning a domain.
Production Redis
-
Provision a Redis instance
- Use any Redis provider, e.g. Redis Cloud, Railway, or a self-hosted instance
-
Add to environment
- Add
REDIS_URL=redis://user:password@host:6379to your.envfile (or set it as an environment secret in production)
- Add
-
Verify
- On the first request to
/api/movie-recommendation, the app logsRate limiter initialized with Rediswhen the connection succeeds - When
REDIS_URLis not set, rate limiting logsREDIS_URL not set. Rate limiting disabled. - When
REDIS_URLis not set, the worker process logs that workers are disabled or unavailable.
- On the first request to
Local Docker PostgreSQL Setup
You can run a fully-configured local PostgreSQL instance with pgvector using Docker Compose — no external provider needed.
Prerequisites
- Docker Desktop installed and running
Local Docker PostgreSQL Steps
-
Run the setup script
npm run setup:local-dbOn first run, this generates a random
POSTGRES_PASSWORD, writes all database credentials to your.envfile, starts the Docker container, and waits until PostgreSQL passes its healthcheck. On subsequent runs it reuses the existing credentials from.envso the running database stays in sync.Docker automatically runs
db/init/01_schema.sqlanddb/init/02_match_movies.sqlon first start, enabling thevectorextension, creating themoviestable, and installing thematch_moviesfunction. -
Sync workspace
.envfilesnpm run copy:envThis copies the root
.envintoapps/web/.envand the local services so the web app, workers, and seed service all use the same credentials. -
Populate the database
npm run populate-db -
Run the app locally
# terminal 1, repo root npm run dev # terminal 2, apps/web cd apps/web npm run start:workersFor the async recommendation flow, keep the workers running while you use the app.
Note: The
pgdatanamed volume persists data across container restarts. Init scripts only run once on first start. To reset the database from scratch, runnpm run cleanup:local-db(stops containers and removes the volume) thennpm run setup:local-dbagain.
Coolify Production Setup
For VPS production deployments, use coolify.compose.yml.
It runs PostgreSQL with pgvector, Redis, the web app, workers, Bull Board, and a
web-start migration step.
See Deploying PopChoice with Coolify for the full setup flow.
If you need to migrate an existing production database manually, run:
DATABASE_URL=<your-production-postgres-url> npm run migrate:db --workspace=apps/webDevelopment Container Setup
This project includes a development container configuration for consistent development environments.
Using with VS Code
-
Install prerequisites
- Install Docker
- Install VS Code Dev Containers extension
-
Set environment variables
- Set environment variables in your shell before launching VS Code
- The dev container will forward these automatically
-
Open in container
- Open the project in VS Code
- Click "Reopen in Container" when prompted
- Wait for the container to build and dependencies to install
Features Included
- Node.js 24 with npm
- Git and GitHub CLI pre-installed
- All VS Code extensions pre-configured
- Port forwarding for development servers (3000, 6006)
- Automatic npm install on container creation
Swapping Database Backends
PopChoice uses a generic DbClient interface (apps/web/src/clients/dbClient.ts) that decouples storage-oriented app code from any specific database provider. By default it uses the PostgreSQL backend via pgClient.ts. Higher-level recommendation/account queries that need joins or upserts live in app-local repositories under apps/web/src/lib/db.
Built-in backend
| Backend | Module | Env var | Notes |
|---|---|---|---|
| PostgreSQL | apps/web/src/clients/pgClient.ts | DATABASE_URL | Default |
How it works
All database operations go through getDbClient(), which returns the active DbClient instance. You can swap the implementation at any time with setDbClient():
import { setDbClient, type DbClient } from '@/clients/dbClient';
const myClient: DbClient = {
isConfigured: () => true,
from: (table) => {
/* return a TableRef that talks to your database */
},
rpc: (fn, params) => {
/* call a stored procedure */
},
};
setDbClient(myClient);Using a mock in tests
import { setDbClient, resetDbClient, type DbClient } from '@/clients/dbClient';
import { afterEach, beforeEach } from 'vitest';
const mockDb: DbClient = {
isConfigured: () => true,
from: () => ({
select: () => Promise.resolve({ data: [{ id: 1, name: 'Mock Movie' }], error: null }),
insert: (rows) => {
const result = { data: Array.isArray(rows) ? rows : [rows], error: null };
return {
select: () => Promise.resolve(result),
then: (onfulfilled?, onrejected?) => Promise.resolve(result).then(onfulfilled, onrejected),
};
},
delete: () => ({ neq: () => Promise.resolve({ data: [], error: null }) }),
}),
rpc: () => Promise.resolve({ data: [], error: null }),
};
beforeEach(() => setDbClient(mockDb));
afterEach(() => resetDbClient());Key files
| File | Purpose |
|---|---|
apps/web/src/clients/dbClient.ts | DbClient interface, getDbClient / setDbClient / resetDbClient helpers |
apps/web/src/clients/pgClient.ts | PostgreSQL (pg) implementation of DbClient with pgvector support |
apps/web/src/clients/dbClient.test.ts | Unit tests demonstrating mock injection |
apps/web/src/clients/pgClient.test.ts | Unit tests for the PostgreSQL backend |
apps/web/src/lib/db/ | App-local repositories for recommendation/account queries |
apps/web/src/utils/database/ | Legacy database utility helpers that use getDbClient() internally |