The Ask
A recovery-community organization ran softball tournaments the way most do: a sign-up form here, a payment collected there, teams picked in a crowded room with a clipboard, and a bracket taped to a wall. It worked, barely, and it didn't scale past the person holding the clipboard.
They wanted one platform that did the whole thing — take registrations and payments, run a live player draft that everyone could watch in real time, generate the brackets, track the games, and sell some merch on the side. Not a website. An operating system for running events.
The two parts that turned this from "a site" into "a real piece of software" were the live draft and the money. Those are the parts where being almost-right isn't good enough.
The Signature Feature: A Live Fantasy Draft
The centerpiece is a fantasy-style player draft. Captains take turns picking from the pool of registered players, in a defined order, on a clock — and everyone in the room (and everyone at home) watches the board update pick by pick, live.
That "live, for everyone, at once" requirement is deceptively hard. It's a multiplayer, real-time, money-adjacent feature where the failure modes are all bad: two captains drafting the same player, a pick that shows up for one person and not another, a clock that disagrees between screens, a draft that silently stalls when someone closes their laptop.
Here's how we made it hold together.
Why we chose polling over "realtime"
The obvious tool for a live feature is a realtime subscription — open a socket, push changes as they happen. We deliberately didn't use Supabase Realtime for the draft. Realtime subscriptions are great until the network isn't: flaky venue Wi-Fi, a phone that sleeps, a reconnection that quietly drops an update. For a draft where a missed update means a wrong roster, "usually delivers" isn't acceptable.
Instead, each draft screen polls the server every two seconds for the current pick number and draft status. It's lower-tech and far more robust — there's no socket to silently die, every screen re-converges on the truth every two seconds no matter what the network did, and a sleeping phone just catches up the moment it wakes. We kept the experience smooth by updating local state in place (only fetching the new picks since the last one the screen knew about) and using refs to dodge stale-closure bugs in the polling loop, with a guard so a slow poll never overlaps the next one. The result feels live without pretending the network is perfect.
Sometimes the boring, reliable choice is the senior one.
A clock that doesn't depend on anyone's browser
Every pick is on a timer. But you can't trust a countdown running in a captain's browser — close the tab and the timer dies with it, and the whole draft hangs waiting on someone who walked away.
So the clock lives on the server. A scheduled job runs continuously, finds every in-progress draft, and compares the time since the last pick against that draft's configured seconds-per-pick. When a pick's time is up, the server acts — the draft moves on whether or not the captain's screen is even open. Three configurable behaviors cover what the organizer wants when the clock hits zero: auto-pick the best available player, skip the pick, or pause the draft.
Auto-pick that's actually smart
When the clock expires on auto-pick, the system doesn't grab a random name. It works down a priority order:
- The captain's queue first — captains pre-rank the players they want, so an auto-pick honors their stated preference and grabs the highest-ranked player still on the board.
- A valid available player otherwise — if the queue is empty or exhausted, it falls back to any eligible registered player.
- Clean exhaustion — when the pool runs dry, the draft marks itself complete instead of erroring.
Throughout, it enforces the rules that matter: captains and co-captains can't be drafted onto another team, already-picked players are excluded, and — critically — only players whose registration is actually paid are eligible. The draft pool and the payment ledger are the same source of truth.
The part that prevents disasters: atomic picks
A live draft with a server-side clock has a nasty hidden risk: a captain clicks "draft" at the exact moment the auto-pick job fires, and two picks race for the same slot. Do that wrong and you double-draft a player or corrupt the pick order — mid-event, in front of everyone.
We pushed the actual pick into a single Postgres function that performs all the related writes — recording the pick, advancing the pick counter, updating the roster — as one atomic transaction. It either all happens or none of it does. The same goes for skips, which advance the counter through an atomic operation rather than a read-then-write that two processes could interleave. The database, not application timing, is what guarantees the draft can't get into an impossible state.
That's the difference between a demo that works when one person clicks slowly and a system that survives a real draft night.
Money Changes Everything
The other half of "real software" is that this platform takes payments — for tournament registration and for merchandise. The moment real dollars move, the bar goes up.
Registration gates the draft. A player registers, pays through Stripe's hosted checkout, and only when Stripe confirms the payment does a webhook flip that registration to paid — which is exactly what makes them eligible for the draft pool. There's no "I'll pay you later" limbo to reconcile by hand; payment status and draft eligibility are one and the same.
Webhooks are verified and defensive. Every incoming Stripe event has its signature cryptographically verified against our webhook secret before we trust a byte of it — no signature, no entry. The handler covers the full lifecycle, not just the happy path: completed checkouts, succeeded and failed payment intents, and refunds. A refund on a charge flows back through the same pipeline so the platform's record matches Stripe's reality.
The merch store is a real store. Products, inventory, cart, checkout — a completed purchase creates an order and decrements stock through the same verified-webhook path. Same discipline, different table.
None of this is glamorous. All of it is what stands between "we collected the money" and "we collected the money, twice, from one person, and can't explain why."
Who Can Touch What
A platform multiple people log into needs real authorization, not just hidden buttons. RecAware has five roles — admin, organizer, captain, player, and customer — and a deliberately collaborative organizer model: any organizer can manage any tournament, not just the ones they created, because on event day whoever's free should be able to help run things. Admins hold the keys to users and settings; captains manage their own team during the draft; players manage their profile and registrations.
The enforcement lives in the database. Every table has Postgres row-level security, so what you're allowed to see and change is decided by the server on every query — hiding a control in the UI is a convenience, not the security boundary. Authentication runs through Supabase Auth (email/password and Google), and even the auth redirects are validated to shut the door on open-redirect tricks.
The Boring-on-Purpose Reliability
The things that don't show up in a screenshot are what make it trustworthy:
- Atomic database transactions for every pick and skip, so a live draft can't be raced into a broken state.
- A fail-closed scheduled job — the draft-timeout endpoint refuses to run unless its secret is configured and the caller presents it, so it can't be triggered by anyone who finds the URL.
- Layered input validation with Zod on the way in, and image uploads checked for size and type before they're processed and stored.
- A real test suite — over a hundred test files across unit, integration, and end-to-end (Playwright) coverage for the flows that matter: auth, registration, the draft, and checkout.
The Stack
Built to run lean on infrastructure that scales to zero when no tournament is live:
- Framework: Next.js 16 + React 19 + TypeScript in strict mode (with the extra-strict compiler checks turned on) — deployed on Vercel.
- Database, auth & storage: Supabase — Postgres with row-level security, Supabase Auth, and avatar storage — with the heavier logic (picks, brackets, standings) living in Postgres functions, across 100+ migrations and 50+ tables.
- Payments: Stripe Checkout for registration and merch, with signature-verified webhooks covering payments, failures, and refunds.
- Scheduled work: a serverless cron job driving the server-side draft clock.
- Media: Cloudinary for product images and team logos.
- Email: Resend for verification, password resets, and order notifications.
- Quality: Vitest + Playwright, with CI running lint, types, tests, and build.
It's a stack chosen so a community nonprofit isn't paying for idle servers between events — serverless compute, a managed database, and pay-as-you-go payments, nothing humming when nobody's playing.
What This Project Showed Us
RecAware looks like a sports website. Underneath, it's some of the hardest categories of work we do:
- Real-time, multiplayer state — a live draft kept consistent across every screen, built on the reliable choice rather than the flashy one.
- Money, handled correctly — Stripe registration and commerce with verified webhooks, refunds, and payment status as a first-class source of truth.
- Concurrency safety — atomic database transactions that make a whole class of race-condition bugs impossible rather than unlikely.
- Authorization that's actually enforced — role-based access backed by database row-level security, not hidden UI.
- Operational discipline — fail-closed scheduled jobs, layered validation, and a test suite that covers the flows where being wrong costs real money.
It's live, it's run by the organization's own people, and it turned a clipboard-and-group-text operation into a platform.
Running Events, Drafts, or Anything With a Clock and a Pool?
A live draft is just one instance of a shape that shows up everywhere: many people acting on shared state in real time, on a timer, where the result has to be correct and consistent for everyone at once — and often with money attached. Auctions, live registrations, scheduling scrambles, anything where "first valid action wins" has to actually mean it.
That's the kind of problem we like. If you've got one, let's talk about it.