CourtOps: Run the Tournament, Not the Bracket
A live tournament-day operations tool that lets organizers run game day by exception instead of firefighting a full schedule.
Project Background
I play a lot of badminton, and I have watched enough local tournaments to notice the same thing every time: the bracket software is great at building the draw and useless once the day actually starts.
A tournament with 9 courts and 60 plus matches does not run on the draw. It runs on the organizer. A match goes 16 minutes long, the next pair has nowhere to play, the court behind it backs up, and a player who showed up on time is now standing around with no idea where to go. The draw software has no opinion about any of this. The organizer holds it all in their head and patches it by walking the floor, texting players, and rewriting the schedule on paper.
So I built the tool for that person. CourtOps runs the day-of logistics, not the draw.

My Roles & Responsibilities
As the sole PM and engineer, I owned the whole thing:
- Framing the real problem (operating the day) versus the obvious one (drawing the bracket)
- Writing the PRD and choosing the north-star metric
- Designing the two-surface system and the signature interaction
- Building the full stack: Next.js, Supabase Realtime, Row Level Security, and a server-side Gemini copilot
The Key Problem
How might we let one organizer run a chaotic tournament day by deciding only on what needs them, instead of re-scanning a full schedule and reacting?
The person on the floor does not need a better bracket. They need to operate by exception: see only the things that need a decision right now, be handed a sensible fix, and have one tap update everything and everyone at once.
The Solution
CourtOps holds two surfaces against one shared state:
- Organizer dashboard (desktop): the command center. What needs you, the court floor plan, the delay radar, the schedule, and the rescheduler.
- Player phone view (mobile, no login): a single calm answer to "when and where do I play next?" that updates itself when the organizer acts.
Try the live demo | View the repo


The thesis: operate by exception
The home screen is Needs You, not a dashboard of everything. It is a feed ranked by the cost of waiting, and it leads with one number: fixes per hour. That is the headline metric on purpose. The job is not to stare at a board. The job is to clear exceptions fast, and the product makes the rate of that work visible.

The signature moment: one action, everything updates
Court 3 opens up. At the same time, a player is warmed up and ready with no court, and a flag is already sitting in the feed saying so. The organizer taps one fix.
In a single action: the floor plan puts the player on Court 3, the flag clears, the fixes-per-hour ticks up, and the player's phone updates by itself to show Court 3. No refresh, no second message, no separate app to keep in sync. One state, two surfaces, broadcast over Supabase Realtime.
That is the whole product in one interaction: the organizer makes one decision and the floor, the schedule, and every affected player move together.

AI suggests, a person decides
The AI never writes to the tournament on its own. Both AI surfaces keep a human in the loop:
- The copilot (Cmd/Ctrl-K): ask in plain language, get back a typed, approvable plan. Nothing changes until the organizer clicks Approve. The approved plan runs through the same code path as a human fix, so the player phone syncs the same way.
- The delay radar: a predictive read of which courts are over plan, when they will realistically free up, and what that pushes downstream. It proposes the fix and the organizer applies it.


Architecture decisions worth calling out
- One store, two surfaces. A single source-of-truth store drives both the organizer dashboard and the player phone, kept in sync through Supabase Realtime. This is what makes "one action, everything updates" real rather than staged.
- RLS role separation. Reads are open to everyone, so the player phone never logs in. Writes require an authenticated session, which is the single organizer account. The permission model maps cleanly onto the two surfaces.
- Frozen clock for stable metrics. The per-second tick stays local and is never written, so the fixes-per-hour denominator does not drift and the numbers stay honest.
- The AI path reuses the human path. An approved copilot plan applies through the same persist code as a manual fix. The AI does not get its own write path, which keeps the human-in-the-loop guarantee structural, not cosmetic.
What I Would Do Next
- Multi-organizer roles and an audit trail of who fixed what.
- Real notification delivery (SMS and push) behind the message-drafting flow.
- Generalize past badminton to other court and field sports.
Reflection
CourtOps is the clearest example of how I work now. The hard part was never the realtime plumbing. It was deciding that the product should be a feed of decisions instead of a dashboard, and that the AI should propose rather than act. Getting those two calls right is what made everything else fall into place.