Back to work
Web app · June 2026 · Shipped

World Cup Pool

A prediction pool app for the 2026 World Cup. Built for my soccer team as a fundraiser, left open to anyone, and running 127 entries by the time it locked at the first kickoff.

You get 30 Pesodollars, buy at least 7 World Cup nations priced 1 to 7 by strength, answer a couple of tiebreakers, and collect points as your teams win. I built it for my soccer team to run as a fundraiser and left it open to anyone who wanted to start or join a pool. 127 entries were in by the time it locked at the first kickoff.

The scoring is simple to say and full of edge cases. A win is 3 points, a draw is 1, a loss is 0. A knockout game decided on penalties is not a draw: the winner takes 3 and the loser still takes 1, because surviving that far is worth something. A win after extra time is just a win. Entries level on points break on the closest guess to total goals across all 104 games, shootout goals excluded, and then on the minute of the first goal in the Final. There is also a separate bonus round of trivia questions running its own prize alongside the main pool.

Anything that decides a winner runs on the server. Scores are never computed in the browser: the leaderboard reads a standings cache that is recomputed server-side after each match, and submitting an entry writes the entry, its teams and its bonus answers through one Postgres function, so a half-submitted squad cannot exist. Every table has row level security, so a participant can read their own entry and the pool leaderboard and nothing else.

It is Next.js on the App Router with Supabase behind it, Server Components for reads and Server Actions for writes, and the same Zod schemas validating on both sides so the client is never trusted. Budget maths is integer Pesodollars throughout, with no floats anywhere near the money. Auth is email magic links, and the whole thing is hosted on Vercel.

Dark only, on a warm grey base with malachite green for anything interactive and trophy gold reserved strictly for first place and winners. Fraunces for headings against Inter for everything else, and tabular figures in the standings so the numbers stop jittering as they update.

The hardest part was what I left out. Payments, paid pool creation and multi-organizer support were all deferred until the scoring engine was provably right on one real pool, which turned out to be the correct call once 127 entries were riding on it.