MyPickleball
MyPickleball is a tournament engine I built for the round robins I was already running myself. Paste in a roster, pick one of ten formats, and it generates the schedule, scores live from any phone, keeps standings, seeds a playoff and pushes the results to DUPR. Around the engine sit the things a club actually needs — events with registration, groups with a message board, a weekly game that drafts itself, and an AI assistant that will run the whole tournament from a chat. Free for anyone with three or fewer tournaments in flight; a Pro plan for people running more.
- Role —
- My own product. Design, build and operation, end to end. Ernesto Martinez contributed the autumn 2025 landing-page redesign and moved the repo from ESLint and Prettier to Biome.
- Timeline —
- February 2024 — ongoing
- Stack —
- Next.js (App Router), React, TypeScript, Prisma + Postgres (Neon), NextAuth (Google, One Tap), Stripe (subscriptions and Connect), Vercel AI SDK via AI Gateway, next-intl (six locales), Tailwind CSS v4 + shadcn/ui, Vercel (crons, Blob), vitest, DUPR Partner and Backend APIs
The problem
Nobody briefed me; the brief was the Tuesday-night problem. I ran a DUPR club called the Slice Ninjas out of the pickleball community at SVB Tennis Center in Zephyrhills, Florida — a community I played in, helped run events for, and later built the website for (SVB Tennis). We leaned toward inclusive formats: ladders, and round robins ranked by DUPR rating. Drawing a round robin by hand is fine until it is not. Someone does not show, or leaves after the second round, and the whole grid has to be redrawn with everyone standing on the court waiting. The tools that existed before 2024 did not have that built in. They were made for sanctioned tournaments, they took a fee on every entry, and they were clunky for eight people on two courts. There was no good way to just spin up a simple matchup. So in February 2024 I started one, from a Prisma and NextAuth tutorial starter, with two jobs in mind: generate matchups fast, and log the scores to DUPR afterwards.
What I built
The first version lived for two and a half years and did what it said. A round-robin generator, the format list growing as the club asked for things — ladder in mid-2024, DUPR score logging that autumn, Major League Pickleball three- and four-player team formats by late 2025 — then player profiles, partner pages, and an events system with Stripe registration in summer 2025. It was a Pages Router app on MUI, and by spring 2026 it carried five hundred commits of features bolted onto a tutorial skeleton. It worked. It had also stopped being something I could extend with confidence.
In June 2026 I rebuilt it, and the whole rebuild fits inside one month and 241 commits. The plan was locked on the seventh: a fresh App Router build with shadcn and Tailwind v4, keep the Postgres database and the Google sign-in, port everything, and use the tournament section I had just built for Wakamole PBC as the architectural reference. The round-robin and single-elimination engines were ported straight from it; the MLP, King of the Court, ladder, double-elimination and pool-play engines were written new against the same shape. Roles, events, community groups, CSV import, the paywall and six languages all landed in the same month. In August the last of the Pages Router and MUI was deleted.
What an organizer sees now: a dashboard of their tournaments grouped under events, and a tournament page with Players, Schedule, Standings, Playoff and Branding panels — a DUPR search typeahead that autofills rating and photo, paste-a-list and CSV import, import-from-a-past-roster, a join code and a spectator QR so nobody watching needs an account, and a chat drawer that can create tournaments, edit rosters, generate rounds and submit to DUPR by talking to it. Formats added since the rebuild include Pana Draft, an NFL-style snake-draft team format with a cross-team DreamBreaker. Beyond tournaments there are events with registration and check-in, city landing pages, groups with channels and threaded messages, and, since September 2026, recurring series — a weekly game that materializes its own event and roster each week for a human to confirm. Paid registration with organizer payouts through Stripe Connect is fully built and not yet switched on. Its sibling is Pickle Prep Pro: the same players, the other side of their week — the minutes before and after a match rather than the tournament itself.
Decisions worth explaining
The engineering, for anyone who wants it.
The engine does not know it is in a web app
Every schedule, standing and bracket computation lives in lib/tournament as plain TypeScript with no React and no database imports. The API routes and the UI call into it; nothing calls out. That is what makes it testable — roughly 160 vitest cases run the formats end to end, including full four- and eight-team double-elimination simulations — and it is what made the rebuild possible at all: the engine could be ported and proven before a single page existed. Re-implementing MLP against tests also surfaced three bugs the old code had shipped for months: pool standings decided a best-of series by the first game alone, ties were broken on wins and nothing else, and the DreamBreaker tiebreak was scored by a rule nobody plays.
One match shape for every format
A rotating-doubles match, an MLP series game, a King of the Court round and a losers-bracket final are all the same row: a tournament id, a phase, a round, a court, two teams stored as arrays of player ids, two scores, a status, and a small JSON field for whatever the format needs to remember. The alternative — a table per format — is the obvious design and the one the first version had drifted toward, and it means every new format is a schema migration, a new set of routes and a new set of components. With a flat shape, Pana Draft went from idea to shipped in a day, because the only new code was the engine.
Substitutes without redrawing the grid
This is the problem the app was started for, and it took until July 2026 to solve properly. Once a schedule existed, the old roster was locked; the honest fix had to wait for an engine clean enough to regenerate part of a tournament. Now a player can be marked absent, a substitute can be attached to the person they replaced, and a late arrival can be added — and the app deletes only the unplayed matches from the next clean round onward and regenerates them with whoever is actually on the court. Completed rounds are never touched; a round with one score already in is treated as played and left alone. Brackets get a walkover instead of a reshuffle. The substitute link is display-only: their history and the original player’s stay separate, so nobody’s record is overwritten.
Never guess a DUPR identity
Submitting to DUPR writes real results against real ratings, and there is no clean undo. So a roster name is never matched to a DUPR id automatically; an id only ever comes from someone picking a result out of the search typeahead. Submission has a dry-run that shows exactly what would go over the wire before anything does, and matches missing any player’s id are skipped and reported rather than filled in. The weekly-series cron that auto-submits results is gated three times over — the roster had to come from the group’s hand-built list, the event had to come from a series, and auto-submit has to be switched on per series. The DUPR client itself de-duplicates concurrent logins, because DUPR rate-limits the login endpoint hard enough that a burst of parallel searches would lock the app out.
A weekly game that drafts itself but never publishes itself
The club plays the same two games every week, and every week used to be a new event, a new tournament, the same roster retyped and results submitted by hand. A recurring series now owns that: a daily cron materializes each upcoming week into a draft event and a tournament shell pre-seeded with the regulars, and the group page shows a confirm card — publish, edit, or skip. Drafts are a nullable publishedAt column rather than a new status value, because a new enum value would have had to be excluded from every existing query while a null fails closed at the six public read paths that were checked one by one. A unique index on series and date makes the cron idempotent: a retry or an overlapping run cannot create the same week twice. Publishing posts the announcement into the group’s channel. Nothing goes public unattended.
Nothing untranslated ships
The app runs in English, Spanish, Chinese, Vietnamese, Japanese and Korean, with English as the reference catalog. A pre-commit check verifies every locale has every key English has, with a non-empty value, and fails the commit if not. Filling the gaps is a script that sends only the missing strings to Claude with instructions to preserve ICU parameters, markup and product names, and never to overwrite an existing translation. The guarantee is structural rather than aspirational — a half-translated page cannot reach production because it cannot be committed.
Free is free, and the money that is not ready stays off
The free plan is the whole product with two limits: three active tournaments at once and three AI-assisted setups a month, both enforced server-side with a 402 so the browser cannot be talked out of them. Completed tournaments never count, so a club can run forever for nothing. Pro is $19 a month or $180 a year and lifts both caps. The other money — entry fees collected per division through Stripe Connect, destination charges with a platform fee, discount codes, refunds, an offline lane for cash on the court — is built through all five planned phases and gated behind a single environment variable that is still false. Until it is on, the register and payout routes answer that payments are not available and the UI says so; nothing half-works. Turning it on is one flag, and it is deliberately the last step rather than the first — the money rails were built while the product around them was still moving, and I would rather they sit finished and dark than ship before the club has a reason to charge anyone.