Competition Management System for Firefighter Combat Challenge — Freestyle Edition, built for a real Polish competition series and used live at events. It replaces manual Excel sheets and paper stopwatches with an admin CRM, a trackside judge tablet app and a public broadcast API consumed by TV graphics software.
Production stack: Next.js 16 (App Router, React 19) · Supabase (PostgreSQL, Auth, Realtime, Storage) · Tailwind CSS 4 + shadcn/ui · TypeScript (strict) · Vercel
- Participant management with WooCommerce order import — athletes register in an online store, the sync maps order fields (unit, county, PB, tandem/relay declarations) onto participant records and never overwrites admin-entered data
- Event hierarchy: zawody (events) → konkurencje (competitions: individual / tandem / relay, per gender) → age-based award categories with post-hoc rankings computed from run times
- Run seeding based on personal bests, drag & drop reordering, dual-lane race pairings
- Tandem/relay pairing dashboard with FCC rule validation (an athlete may enter at most one team per category plus one MIX team), fuzzy-matching suggestions from declared team names and a "problems" card that explains every incomplete or invalid squad in plain language
- Withdrawn-athlete flag (injured no-shows stay in the system, excluded from seeding), CSV exports (Excel-friendly: BOM, semicolons, CRLF, formula-injection guard) including a full verification export used to reconcile app data against store orders
- Full audit log of every mutation
- PIN-based judge authentication (bcrypt), independent from admin auth
- Touch-first UI for recording times and penalties trackside, designed for outdoor tablet use (large targets, high contrast)
- Supabase Realtime keeps all judges, admins and displays in sync; admins can reassign judge lanes live and the tablet reacts instantly
Token-protected JSON endpoints consumed by the TV production team (vMix/OBS):
scoreboard— index of events with paste-ready per-category scoreboard URLsscoreboard/[categoryId]— ranked results in the broadcaster's contract format:[{"miejsce":"1","nazwisko":"…","czas_koncowy":"MM:SS.hh"}], penalties included, DNF rowsbelki— lower-third data for the run currently on track (red/blue lane athlete + unit), updates as judges select runnerstop10— live best times for the venue big screen
- Server Components fetch, Client Components interact — pages query Supabase server-side and pass initial data down; mutations run through typed service classes (
ParticipantsService,RunsService,JudgesService) or server actions - Typed database end-to-end —
database.types.tsgenerated from the schema,Database['public']['Tables'][…]types flow through services, components and Zod validation schemas - Row Level Security baseline with a column-level GRANT trick to expose judge rows over Realtime without leaking PIN hashes
- 30 SQL migrations in
supabase/migrations/document the schema evolution from a single participants table to the full event hierarchy - Vitest unit suite covering ranking engine, FCC membership validation, CSV builders and broadcast time formatting
app/ Next.js application (deployed to Vercel)
supabase/ PostgreSQL migrations
cd app
npm install
cp .env.example .env.local # fill in your Supabase project keys
npm run devRequired environment variables:
| Variable | Purpose |
|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Supabase project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Supabase anon key |
SUPABASE_SERVICE_ROLE_KEY |
server-side service role key (public API, seeding) |
JUDGE_SESSION_SECRET |
HMAC secret for judge session cookies |
PUBLIC_RESULTS_API_KEY |
bearer/query token for the broadcast API |
Apply the migrations from supabase/migrations/ to your Supabase project in filename order.
The product UI is in Polish (the customer's language); code, comments and schema are in English.