93 lines
4.1 KiB
Markdown
93 lines
4.1 KiB
Markdown
# 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
|