# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## What This Is MulliganMates is a social golf score tracking PWA. Multiple players in a group track scores per hole in real-time. Anyone in a group can enter or edit scores for any player. Built as a self-hosted Next.js app running in Docker Compose with a single PostgreSQL database. ## Commands All commands run inside Docker (Node.js is not installed on the host): ```bash # Development (with Mailpit for email) docker compose --profile dev up # Production docker compose up -d # Install a new npm package docker run --rm -v $(pwd):/app -w /app node:22-alpine npm install # Add a shadcn/ui component docker run --rm -v $(pwd):/app -w /app node:22-alpine npx shadcn@latest add # Type-check / build docker run --rm -v $(pwd):/app -w /app node:22-alpine npm run build # Generate Drizzle migrations (after schema changes) docker run --rm -v $(pwd):/app -w /app -e DATABASE_URL=... node:22-alpine npx drizzle-kit generate # Apply migrations manually docker run --rm -v $(pwd):/app -w /app -e DATABASE_URL=... node:22-alpine npx drizzle-kit migrate ``` ## Architecture **Stack:** Next.js 15 (App Router) · TypeScript · Tailwind CSS v4 · shadcn/ui v4 · better-auth · Drizzle ORM · SSE · nodemailer · Serwist (PWA) **Routing groups:** - `app/(auth)/` — unauthenticated pages (login only) - `app/(app)/` — authenticated app shell with bottom navigation **Auth (`lib/auth.ts`):** - better-auth with `magicLink` plugin (`disableSignUp: true` for invite-only) - `drizzleAdapter` pointing to `lib/db/schema.ts` tables: `user`, `session`, `verification` - Additional field: `handicapIndex` on user - Server-side session: `auth.api.getSession({ headers: await headers() })` - Magic link to invite new users: create user with `db.insert(user)` first, then `auth.api.signInMagicLink()` **Database (`lib/db/`):** - `index.ts` — `drizzle(postgres(DATABASE_URL))` export - `schema.ts` — all table definitions (camelCase column names) - `migrations/` — generated SQL files applied via `instrumentation.ts` on startup - **All Drizzle field names are camelCase** (`holesCount`, `courseHandicap`, `holeNumber`, `strokeIndex`) **Real-time scores:** - `app/api/rounds/[id]/stream/route.ts` — SSE Route Handler polling `scores` table every 3s - Client connects via `new EventSource('/api/rounds/{id}/stream')` - Payload: `ScoreRow[]` with `{ holeId, playerId, strokes }` **Round lifecycle:** `lobby → active → completed` - Lobby: players join/are invited; no scoring - Active: started by round creator; scoring enabled - Completed: closed by creator; read-only ## Database Schema Key Points - better-auth tables use `text` PKs (nanoid); app tables use `uuid` PKs (`gen_random_uuid()`) - `scores` has unique constraint on `(roundId, holeId, playerId)` — upsert pattern - `roundPlayers` snapshots `handicapIndex` and `courseHandicap` at join time - Migrations run automatically on app startup via `instrumentation.ts` ## UI Conventions - shadcn/ui v4 uses `@base-ui/react` — **no `asChild` prop on Button**. Use `buttonVariants()` + `` instead - Net score vs par (WHS handicap-adjusted) is always **primary** display, bold - Raw strokes vs course par is secondary - Score entry defaults to hole par; +/− buttons; tap number for direct input - Bottom navigation: Rounds · History · Profile (+ Admin tab for admin user) - Mobile-first: minimum 44×44px tap targets, swipe between holes **Handicap calculation (WHS):** `course_handicap = handicap_index × (slope_rating ÷ 113) + (course_rating − par)` See `lib/handicap.ts` for `strokesReceived()`, `netStrokes()`, `formatVsPar()`, `scoreColorClass()` ## Infrastructure - **Docker Compose** — `app` + `db` (postgres:17-alpine) + `mailpit` (dev profile) - **Traefik** — external reverse proxy; services expose via labels only - **Email dev:** Mailpit (`docker compose --profile dev up`) - **Email prod:** Set `SMTP_HOST`, `SMTP_USER`, `SMTP_PASS` in `.env` - Copy `.env.example` → `.env` before first run - `BETTER_AUTH_URL` must equal the app's public URL for magic links to work