Pickleball Tournaments
A web app for running pickleball tournaments: register as an admin, create a
tournament, set up doubles pool play, generate the match schedule across your
courts, and share one public link where anyone can follow live results and
group standings — no sign-in required.
New to the project? Read HANDOVER.md for the concepts,
architecture, decisions, operations runbook and known gotchas.
Stack
- Next.js 16 (App Router) + TypeScript
- Prisma ORM + PostgreSQL (Neon / Vercel Postgres in production)
- Auth.js (NextAuth v5) — email/password admin accounts
- Tailwind CSS — navy blue theme, mobile-first
- SWR — polling for live public results
Local development
-
Install dependencies:
npm install
-
Set up a Postgres database and put its connection string in .env
(used by the Prisma CLI) — for local development without installing
Postgres yourself, you can use Prisma's local dev server:
npx prisma dev
This prints a postgres://... connection string — put it in .env as
DATABASE_URL. Copy .env.example to .env.local for the rest of the
variables (AUTH_SECRET, NEXT_PUBLIC_APP_URL).
-
Run migrations:
npx prisma migrate dev
-
Start the dev server:
npm run dev
How it works
Phase 1 — pool play
- An admin registers, creates a tournament, and gets a public link
(
/t/<slug>) that anyone can view live, with no login.
- The admin adds courts and enters the total player count — any even
number from 8 to 96 (doubles). The app picks a power-of-2 number of
groups (so the bracket in Phase 2 never needs byes) with 3-6 teams per
group, and spreads the teams across them as evenly as possible — see
planGroupSizes in src/lib/groupPlanning.ts. The app automatically
creates the groups and labels every team (A1–A4, B1–B6, …).
- The admin generates the match schedule: each group plays full round robin,
matches are distributed across courts (5-team groups are ordered so no team plays twice in a row), and a "round" is one wave of
matches — a court's next match only becomes the next round once every
court has a match in the current round.
- Courts are set up in two lists: group stage courts (pool play) and
elimination courts (knockout rounds), so e.g. 4 courts for pool play and
3 for the bracket. Leave the elimination list empty to reuse the group stage
courts. Group stage courts lock once the schedule is generated; elimination
courts stay editable until the bracket is generated. Court names can be edited
(Edit next to each court) at any time, even after matches are generated or
played; matches and the public page pick up the new name. Each match also has an
Edit link next to its court to move that single match to another court (group
stage courts for pool matches, elimination courts for knockout matches), even
after it has been played; a note appears if another match in the same round is
already on that court.
- The admin enters a single game score per match (e.g. 11-7); winners and
group standings (points, wins/losses, point differential) are computed
automatically. Best-of-N / multi-set scoring is planned for Phase 4, when
the admin can configure the match format.
- The public page has two tabs — Match Results (with a sub-tab per
round) and Group Standings — both auto-refreshing every few seconds.
Phase 2 — elimination bracket
- Once every pool match is complete, the admin can generate the elimination
bracket from a button on the Matches panel.
- The admin chooses how many teams advance when creating the groups (the
"Teams advancing" dropdown next to the player count). The choices are the
powers of 2 the groups allow, e.g. 4, 8 or 16 teams for 4 groups — which
works out to the same number of teams from every group (top 1, 2 or 4).
Default is the top 2 per group. See
src/lib/advancing.ts. It can be changed
from the Teams advancing card at any time until the bracket is generated.
The group standings (public and admin) bold exactly the teams that qualify.
- First-round seeding gives all group winners seeds 1..G, second place
G+1..2G, and so on, then pairs seed s against seed N+1-s (see
seedFirstRound in src/lib/bracket.ts), so no team can face an
opponent they already played in pool play, in round 1.
- Bracket size is always a power of 2 (the group count and the advancing
count both are), so there's never a need for byes.
- Once every match in a knockout stage (Semifinal, Quarterfinal, Round of
16) is complete, the app automatically generates the next stage by
pairing consecutive winners in bracket order (
src/lib/bracket-progress.ts)
— no extra admin action needed between rounds. When the Semifinal completes,
it generates both the Final (winners) and a 3rd Place match (the two
semifinal losers play each other), on different courts when there are
several so they can be played at the same time. The champion and 3rd
place are shown on the admin and public pages as soon as each match is
done, and the tournament is marked COMPLETED only once both the
Final and the 3rd Place match have been played.
- Manual re-pairing: until any match in a knockout round has a result, the
admin can click Edit pairings on that round (Round of 16, Quarterfinal or
Semifinal) and choose who meets who. Every team must be used exactly once, and
a warning shows if two teams from the same group are paired. The round locks as
soon as one of its matches has a result; the Final and 3rd Place match are not
editable because the results decide them.
- Player names: once groups exist, the admin can enter each team's two
player names (optional, editable any time). Once set, they show up
anywhere a team label appears on the public page and in the admin match
views, e.g. "A1 (John & Jane)" — falling back to just the label
otherwise.
This is the beginning of the README. Read the rest at the original. Read the full README ↗