Files
MulliganMates/CLAUDE.md
T
2026-03-18 12:37:17 +01:00

93 lines
4.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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):
```bash
# 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, never `getSession()` — 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/confirm` route handler exchanges the PKCE code for a session.
- `middleware.ts` guards all `/(app)` routes; redirects unauthenticated users to `/login`.
- Admin email is set via `ADMIN_EMAIL` env var; `lib/admin.ts` exposes `isAdminEmail()`.
**Real-time scores:**
- Supabase Postgres Changes subscriptions on the `scores` table, filtered by `round_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 policies
- `service_role` key 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)
- `studio` and `mailpit` services are in the `dev` profile — not started in production
- **Email dev:** Mailpit catches all outgoing email (`SMTP_HOST=mailpit`)
- **Email prod:** Set `SMTP_HOST`, `SMTP_USER`, `SMTP_PASS` to Resend/Mailgun/etc. in `.env`
- Copy `.env.example``.env` and 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