Files
MulliganMates/CLAUDE.md
T
2026-03-19 09:09:55 +01:00

4.0 KiB
Raw Blame History

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):

# Start
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 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.tsdrizzle(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/reactno asChild prop on Button. Use buttonVariants() + <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 Composeapp + db (postgres:17-alpine)
  • Traefik — external reverse proxy; services expose via labels only
  • Email: Set SMTP_HOST, SMTP_PORT, 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