← All work

Wakamole PBC

Wakamole PBC is Coach David “Coach D” Rosales’s pickleball club in Tampa Bay, and wakamolepbc.com is three products under one roof: the club’s public site, an invite-only CRM where coaches run their lessons, and a tournament platform that registers players, takes their money, draws the rounds, keeps score and sends the results to DUPR. It started as a site for a coach who did not have one. A year later it runs the club’s weekly Moneyball nights, and the tournament engine at its core became the one MyPickleball was rebuilt on.

Role —
Design and build, end to end, for a client who is also my coach and my friend. Ernesto Martinez built most of the lesson CRM — the February to April 2026 phase — and I built the rest.
Timeline —
June 2025 — ongoing
Stack —
Next.js (App Router), React, TypeScript, Postgres via Drizzle, Supabase Auth (Google, magic link), Stripe Checkout, DUPR Partner API, Resend, Sanity CMS, next-intl (six locales), Vercel (crons), vitest
Visit wakamolepbc.com

The problem

David did not have a website. He was coaching one-on-ones and running morning drilling sessions out of four facilities across Tampa Bay, booking everything by message, and he was not running Moneyball nights at all. I am one of his students, so the brief arrived the way a friend’s brief does: a site that made the club look like what it was, that let people book him and his coaches, and that could grow into whatever the club needed next. Since it shipped, the site has brought the club traffic and real revenue, and “whatever the club needed next” turned out to be a tournament platform. The weekly Moneyball nights — rating-capped doubles for cash, thirty dollars early and forty at the door — needed registration, payment, a fair draw, live standings and DUPR results, and no off-the-shelf tool did all five for a club this size without taking a cut of every entry.

What I built

Phase one, from June 2025, was the club site: lessons, drilling, camps, the coaches with their own pages, the four partner facilities with their amenities, a shop, DUPR assessments, testimonials, and a nine-question intake form that qualifies a lead before anyone replies. Coaches and products lived in a Sanity CMS so David could edit them; Stripe Checkout took deposits. In January 2026 the site picked up six languages, a round-robin calendar and a proper Open Graph pass. Its partner-deals page later seeded the discounts directory of La Casa de Pickleball, David’s Spanish-language publication with Carlos Zambrano.

Phase two, February to April 2026, was the lesson CRM, and most of it is Ernesto’s work: an invite-only dashboard behind Google sign-in where each coach sees their own appointments and clients, an admin view for David with commissions, refunds, webhook reconciliation and coach availability, built with a repository layer, Zod at every boundary, a test suite and a written QA checklist. Payments all go to David, who pays his coaches himself; Stripe Connect was deliberately left for later.

Phase three began on June 6, 2026, with a pure tournament engine — rotating, fixed and mixed doubles, pools into a bracket, standings — and in the next two days grew the whole /play platform around it: player accounts with magic-link sign-in, events with Stripe registration and thirty-minute holds, waitlists, partner pairing, text-blast and email comms, an organizer dashboard, and an installable PWA. July added self-registration, invites, custom payment requests, comps and day-of pricing. August added the alerting layer and a payment-reconciliation cron, then DUPR: link a roster to DUPR IDs by CSV, pull verified ratings, seed pools by them, and submit every completed match back to DUPR exactly once. September added withdrawals, substitutes and forfeits. The organizer guide for all of it is served in the app at /play/help, written so that someone other than me can run a night. The platform has run dozens of nights so far, including the club’s one-year Anniversary Moneyball in August 2026.

Vertical timeline of Wakamole PBC in three phases: the club site from June 2025 to January 2026, the lesson CRM from February to April 2026, and the tournament platform from June 2026 onward, including the night run on paper on September 9 and the withdrawals feature that followed.
Sixteen milestones in three phases: the club site, the lesson CRM, and the tournament platform. The red phase is the one that turned a coach’s website into the system his club runs on — and the one MyPickleball’s engine was ported from.

Decisions worth explaining

The engineering, for anyone who wants it.

Architecture diagram: the public site, the invite-only coach CRM and the /play tournament platform all read from one Postgres database through Drizzle and share Supabase Auth; the /play API routes call the pure tournament engine, Stripe, DUPR, Resend email and Instagram; Vercel crons expire checkout holds and reconcile Stripe payments against registrations.
Three front doors — the public site, the coach CRM and the /play platform — over one Postgres database and one auth system. The pure tournament engine in red has no framework imports; the API routes call it and it calls nothing. Stripe, DUPR, email and Instagram sit behind the routes; two crons release abandoned holds and reconcile payments.

The night that was run on paper

On September 9, 2026, a fixed-doubles Moneyball ended up run by hand. Some players had not registered through the site, so their names were typed onto the roster; the only way to take someone off was a trash icon that deleted their row and silently left a one-person team, and the generator drew those as doubles sides. The guards were not there and the night had not been rehearsed against the software beforehand. The fix came in three parts over four days: a teammate can no longer be deleted alone and a half-empty team can no longer be drawn; every typed name now resolves to one identity — linked to an account, reused from the roster, or the organizer is asked “did you mean…?” — never a silent duplicate; and then the right tool, which had not existed: withdraw a team, replace a partner, reinstate them. A withdrawn team’s unplayed matches become forfeits, a win for the opponent with zero points both ways, never sent to DUPR; played matches are never rewritten. A training walkthrough ships with it so the next organizer can rehearse pull-outs on a scratch tournament in five minutes.

The engine does not know it is in a web app — and it moved house

Schedules, standings and brackets live in lib/tournament as plain TypeScript with no framework or database imports, exercised directly by unit tests and called by the API routes. That is what let the same engine be lifted out and ported into MyPickleball’s June 2026 rebuild, where it grew MLP, King of the Court and ladder formats on top. Here it stayed focused on what the club actually runs — rotating, fixed and mixed doubles, pools seeded by rating into a single-elimination bracket — and grew the things a real night needs instead: forfeits, roster identity, and a draw the organizer can edit by hand when the generator’s pairing is not the one they want.

A hold is not a sale

When a player starts checkout, their spot is held for thirty minutes and shown as Unpaid to the organizer. If they never finish, a cron releases the hold so it stops consuming capacity and the next person on the waitlist is offered the place. Payment is confirmed by the Stripe webhook, not by the browser landing on a success page — a player who closes the tab still gets registered, and a player who lands on the success page without a recorded payment is told the truth rather than shown a confirmation. Checkout amounts are validated on the server against the tournament’s own prices, so the client cannot name its own fee, and a player cannot hold two payable sessions for the same night.

Know within minutes when money and registration disagree

For a year the app had no observability: a hundred console.error calls scrolling past in a log nobody read, and a real failure mode where a player could pay through Stripe and end up with no registration. The alerting layer replaced that with structured logs, an email alert on every critical path — webhook failures, checkout creation failures, paid sessions with no active registration, confirmation emails that did not send — throttled by fingerprint so a storm becomes one message, and an hourly cron that reconciles Stripe’s paid sessions against the database and reports orphans. No error-tracking SaaS: email is the channel, because the person who needs to know is the organizer at the courts. The runbook lists every alert and what to do about it.

Verified ratings beat self-reported ones, and every rating says where it came from

Pools are drafted strongest-first and snaked so each pool gets a spread of ability, which only works if the ratings are honest, and self-reported ratings skew high. So a player’s DUPR ID can be linked by CSV or by the player themselves, their verified doubles rating and profile photo are pulled from DUPR, and every rating on a roster carries a badge — verified from DUPR, self-reported, or typed by the organizer — with a fixed precedence between them. Results go the other way too: on a DUPR-rated night a floating button submits every completed match exactly once, previews what will be sent and what will be skipped, records the club each match went to, and freezes a submitted match so the two systems can never silently disagree.

One guide, served in the app

The organizer guide is not a PDF in a drive. It is a page at /play/help, reached from the organizer dashboard, written for someone who has never run an event — “if you can use a phone, you can do this” — and it is the same text as ORGANIZER_GUIDE.md in the repository, so shipping an organizer-facing feature means editing both. The playbook beside it covers the one thing that goes wrong at every event, someone who cannot play, as nine scenarios that each name the exact button. The point of both is that the club should be able to run a night without me there.

← All work