4.1 KiB
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):
# 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 <package>
# Add a shadcn/ui component
docker run --rm -v $(pwd):/app -w /app node:22-alpine npx shadcn@latest add <component>
# 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
magicLinkplugin (disableSignUp: truefor invite-only) drizzleAdapterpointing tolib/db/schema.tstables:user,session,verification- Additional field:
handicapIndexon user - Server-side session:
auth.api.getSession({ headers: await headers() }) - Magic link to invite new users: create user with
db.insert(user)first, thenauth.api.signInMagicLink()
Database (lib/db/):
index.ts—drizzle(postgres(DATABASE_URL))exportschema.ts— all table definitions (camelCase column names)migrations/— generated SQL files applied viainstrumentation.tson startup- All Drizzle field names are camelCase (
holesCount,courseHandicap,holeNumber,strokeIndex)
Real-time scores:
app/api/rounds/[id]/stream/route.ts— SSE Route Handler pollingscorestable 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
textPKs (nanoid); app tables useuuidPKs (gen_random_uuid()) scoreshas unique constraint on(roundId, holeId, playerId)— upsert patternroundPlayerssnapshotshandicapIndexandcourseHandicapat join time- Migrations run automatically on app startup via
instrumentation.ts
UI Conventions
- shadcn/ui v4 uses
@base-ui/react— noasChildprop on Button. UsebuttonVariants()+<Link>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_PASSin.env - Copy
.env.example→.envbefore first run BETTER_AUTH_URLmust equal the app's public URL for magic links to work