Fair Play — Golf Scoring, Handicap & Competition Platform
Full-stack golf platform with WHS handicap, social feed, competitions, and offline scoring
Demo coming soon
Overview
Fair Play is a full-stack golf platform that grew out of a personal need to track rounds and handicap, then expanded into a social and competitive product. It supports hole-by-hole scorecard entry with live score-to-par feedback (strokes, putts, fairways, GIR), a global course database, round history with shareable OG-tagged scorecards, a friends graph with an activity feed and leaderboards, and custom competitions with NET/GROSS modes and push notifications. Scoring works offline and syncs automatically when reconnected.
Architecture
The backend is an Express 5 API with Prisma 6 ORM on Supabase PostgreSQL, organised into seven route groups (auth, courses, rounds, handicap, friends, competitions, notifications). Authentication uses JWT plus Google OAuth, with bcrypt password hashing, Zod request validation, per-IP rate limiting, and Resend-powered email verification. Push notifications run on web-push with VAPID. The frontend is React 19 + Vite with Material UI v6, React Router v7, and TanStack React Query for server state; an Axios interceptor handles silent token refresh, and an IndexedDB queue replays offline write requests on reconnection. The schema uses a RoundHole join model so par values always resolve from the Hole record, preventing historical corruption when courses update. Competition status is derived from dates rather than stored mutably.
Key Challenges
- •Implementing the World Handicap System formula correctly (score differentials, best-8-of-20 averaging, 0.96 adjustment)
- •Designing offline-first scoring with an IndexedDB request queue that replays on reconnect without duplicates
- •Building a competitions module with NET/GROSS modes, date-window eligibility, and automatic handicap adjustments
- •Wiring web-push VAPID notifications for competition invites and submissions across devices
- •Designing the RoundHole join model so par data never gets duplicated across historical rounds
- •Handling Supabase connection pooling constraints (port 6543 transaction pooler vs direct port 5432)
- •Express 5 parameter typing quirks and route ordering (e.g. /rounds/stats vs /rounds/:id)
What I Learned
- •Designing offline-first PWAs with IndexedDB request queueing and conflict-safe sync
- •Prisma 6 + PostgreSQL schema design, migrations, and query optimisation
- •React Query patterns for server state, cache invalidation, and silent token refresh via Axios interceptors
- •Implementing Google OAuth alongside email/password auth with email verification
- •Web Push / VAPID notification delivery and subscription management
- •Full-stack TypeScript with shared type patterns between frontend and backend
- •World Handicap System specification and sports scoring domain modelling
Future Improvements
- •Add course rating and slope rating data for more accurate handicap calculations
- •GPS-based course detection and per-shot tracking
- •Richer competition formats (match play, stableford, team events)
- •Native mobile wrapper with background sync