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 + Supabase app running in Docker Compose.
Commands
All commands run inside Docker (Node.js is not installed on the host):
# Development (with Studio + Mailpit)
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
docker run --rm -v $(pwd):/app -w /app node:22-alpine npm run build
Architecture
Stack: Next.js 15 (App Router) · TypeScript · Tailwind CSS v4 · shadcn/ui · Supabase (self-hosted) · TanStack Query v5 · Zustand · Serwist (PWA)
Routing groups:
app/(auth)/— unauthenticated pages (login, confirm)app/(app)/— authenticated app shell with bottom navigation
Supabase clients:
lib/supabase/client.ts— browser client (use in'use client'components)lib/supabase/server.ts— server client + admin client (Server Components, Server Actions, Route Handlers)- Always use
supabase.auth.getUser()server-side, nevergetSession()— getUser() cryptographically verifies the JWT
Auth flow:
- Invite-only. New users are invited via
supabase.auth.admin.inviteUserByEmail()(uses service role key). - Magic link →
/auth/confirmroute handler exchanges the PKCE code for a session. middleware.tsguards all/(app)routes; redirects unauthenticated users to/login.- Admin email is set via
ADMIN_EMAILenv var;lib/admin.tsexposesisAdminEmail().
Real-time scores:
- Supabase Postgres Changes subscriptions on the
scorestable, filtered byround_id. - TanStack Query cache is invalidated on change events.
- Score entry uses UPSERT —
unique(round_id, hole_id, player_id)constraint.
Round lifecycle: lobby → active → completed
- Lobby: players can be invited and join; no scoring yet.
- Active: started by round creator; scoring enabled; no new players.
- Completed: closed by round creator; read-only.
Database
Migrations live in supabase/migrations/ and are applied on DB container startup via /docker-entrypoint-initdb.d/.
00001_schema.sql— tables, triggers (auto-create profile, enforce course par sum, update scores.updated_at)00002_rls.sql— Row Level Security policies for all tables
Key RLS rules:
- Scores can be inserted/updated by any round member, but only when
round.status = 'active' is_round_member(round_id)helper function used throughout RLS policiesservice_rolekey bypasses RLS — only use server-side, never expose to client
Course par validation: A DB trigger fires on holes INSERT/UPDATE; once 9 or 18 holes exist for a course, it asserts SUM(holes.par) = courses.par.
Handicap calculation (WHS):
course_handicap = handicap_index × (slope_rating ÷ 113) + (course_rating − par)
Stored as a snapshot on round_players at join time.
Infrastructure
- Docker Compose — all services defined in
docker-compose.yml - Traefik — external reverse proxy; services expose themselves via labels only (no port mappings)
studioandmailpitservices are in thedevprofile — not started in production- Email dev: Mailpit catches all outgoing email (
SMTP_HOST=mailpit) - Email prod: Set
SMTP_HOST,SMTP_USER,SMTP_PASSto Resend/Mailgun/etc. in.env - Copy
.env.example→.envand fill all values before first run
Key Conventions
- Net score vs par (WHS handicap-adjusted) is always the primary display, in bold
- Raw strokes vs course par is secondary
- Score entry defaults to hole par; +/− buttons; tap number for picker
- Bottom navigation: Rounds · History · Profile (+ Admin tab for admin user only)
- Mobile-first: minimum 44×44px tap targets, swipe between holes, high-contrast for sunlight