Back to Projects

Fair Play — Golf Scoring, Handicap & Competition Platform

Full-stack golf platform with WHS handicap, social feed, competitions, and offline scoring

React 19TypeScriptViteExpress 5Prisma 6PostgreSQLMaterial UIReact Query

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