Skip to content
back to work
Sole PM & Engineer

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.

Product StrategyRealtime SystemsHuman-in-the-loop AIFull-stack
RoleSole PM & Engineer
Timeline2026

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.

CourtOps landing page: 'Run the day, not just the draw' with three pillars: operate by exception, one action everything updates, AI suggests a person decides
The whole pitch in one line: run the day, not just 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:

  1. Organizer dashboard (desktop): the command center. What needs you, the court floor plan, the delay radar, the schedule, and the rescheduler.
  2. 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

CourtOps court board: a floor plan of 9 courts showing live, match-ready, and idle states at a glance
The organizer's command center: a live floor plan of every court, color-coded by what needs attention.
The player phone view shown in a phone mockup: 'My next match' with the upcoming match, court, and day schedule, no login required
The other surface: a player's phone answers 'when and where do I play next?' and updates itself, no login, no asking around.

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.

CourtOps 'Needs you' screen: a decision feed ranked by cost of waiting, with Fixes/hr, Need you, and Cleared as the masthead numbers, and a selected flag with a recommended fix and an AI-drafted message
The thesis in one frame: a ranked feed of only what needs a decision, led by 'fixes per hour'. Each item comes with a recommended fix and a drafted message to send.

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.

CourtOps reschedule screen: a proposed plan that rearranges three matches, with guardrails (no player double-booked, 10 min rest, no court idle, finals finish by 4:30) and an impact summary, plus one message that covers all affected players
When one court runs long, CourtOps proposes a reshuffle that respects the guardrails, shows the downstream impact, and notifies everyone affected with a single message.

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.
CourtOps copilot: a Cmd/Ctrl-K command bar reading 'Ask CourtOps to fix something' with plain-language example prompts like 'Court 5 is open, who should go on next?'
The copilot: ask in plain language and get back a typed, approvable plan. Nothing changes until you approve it.
CourtOps delay radar: live matches ranked by minutes over plan, a selected match showing behind plan, projected finish, and what it pushes downstream, with a recommended fix and 'Apply fix & notify' button
The delay radar: which courts are slipping, by how much, what it pushes downstream, and the one fix that absorbs it.

Architecture decisions worth calling out

  1. 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.
  2. 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.
  3. 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.
  4. 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.