Back to work
Footy26

Case study · Footy26

One summer. 104 matches. 20,451 predictions.

Footy26 is a World Cup 2026 bracket game I designed, built, and operated end to end, as a nights-and-weekends passion project. 273 people played. 146 finished a full 104-match bracket. 43 of them called Spain.

players
273
complete 104-match brackets
146
predictions scored
20,451
days live on the tournament clock
39

The TL;DR

What the season proved

Eight learnings, each earned in production, spanning the five pillars of the project: data, marketing, product insight, engineering, and entrepreneurship. The chapters below tell the step-by-step story behind every one.

  1. 01

    A deadline sells better than any feature.

    60% of players joined in kickoff week; complete brackets rose 55% on deadline day

    Chapter 3 Marketing
  2. 02

    Friends are the growth channel.

    80% joined a friend league; 7 in 10 arrived in a league someone else created

    Chapter 4 Marketing
  3. 03

    Started means finished when the path is short.

    81% of bracket builders completed all 104 exact-score picks

  4. 04

    Ship on the signal, mid-season.

    The bonus-pick prompt converted 87% of the players it reached

  5. 05

    A live event is a retention engine.

    9.5-minute sessions at a 22.5% bounce rate; week-6 return still at 9.9%

  6. 06

    The pocket is the product.

    86% of player pageviews were mobile; the desktop share in my raw data was mostly me

  7. 07

    Operate like production from day one.

    A day-2 framework race was contained in 48 hours: recovery first, then root cause, then a guard test

    Chapter 4 Engineering
  8. 08

    Build the platform, ship the tenant.

    The chassis is sport-agnostic; every season reviewer asked for the next bracket

    Chapter 6 Entrepreneurship

The rates a growth team asks for

visitor to account conversion
45%
of builders finished all 104 picks
81%
of players arrived in kickoff week
60%
bonus-prompt conversion
87%
week-1 return rate, kickoff cohort
22.7%
bounce rate across the run
22.5%
average session length
9.5 min
joined a friend league
80%

Chapter 1 · Entrepreneurship

The bet

The 2026 World Cup was the biggest ever staged: 48 teams, 104 matches, hosted across the United States, Canada, and Mexico. The prediction games built for it split the group stage from the knockouts, buried the fun under ads, or treated playing with your actual friends as an afterthought. My bet: one clean bracket, built around private friend leagues, could turn casual viewers into a summer-long ritual.

This was a project built for fun, passion, and learning, not money. Free to play, no ads, no paid tier; the only revenue surface is an optional tip jar. That freedom set the quality bar: every decision could optimize for the player.

Chapter 2 · Engineering

Built in the margins

First commit May 1. First kickoff June 11. The six weeks in between were nights and weekends around a full-time role: feature-complete for a friends beta about a month before kickoff, then hardened for strangers in the final stretch. What shipped: 47 routes in 12 languages, an installable PWA, one-tap Google sign-in, the 104-match bracket with its cascading knockout tree, bonus picks, private leagues with invite links, live leaderboards, a share-card studio, an AI news desk, a public stats hub, and a penalty-shootout mini game standing in for the 404 page.

A design system, not a pile of screens

Every surface draws from one hand-rolled token catalog: an OKLCH palette, type scale, and status colors in a single theme block, consumed by a small shared primitive layer. Cross-surface consistency is a written contract, not a habit: one documented pick-vs-result grammar covers every place a pick is rendered, and a unit test pins the shared treatment so surfaces cannot silently drift.

Shared data, private by construction

A social game runs on shared data, so privacy is structural, not polite. Row-Level Security is forced on all 21 tables; social surfaces share counts, never values (a league roster shows "98 of 107 picks", never the picks); and every table’s exposure is declared in a manifest that CI verifies against the real schema on every change. A future competition cannot inherit the World Cup’s visibility rule by accident: the rule is a required field with no default. 148 adversarial probes and a weekly fresh-context audit kept the whole thing honest.

Pressing share

The hardest ship of the six weeks was not technical. Posting a personal project to LinkedIn, something made for the love of it rather than for work, took real vulnerability. I hit publish in late May, once a two-day adversarial security audit signed off the product as safe to promote to strangers.

Security by construction

01

Row-Level Security forced on all 21 tables, every write routed through locked-down SECURITY DEFINER functions, and column-level triggers guarding server-owned fields. 148 adversarial attack probes run in CI, and a fresh-context audit re-derived the threat model from scratch every week of the live window.

A scoring engine proven by simulation

02

The 104-match scoring rules live in TypeScript and SQL, and CI plays an entire simulated tournament (eight seeded players, penalty shootouts included) to assert the two engines agree to the point. 20,451 real predictions were scored without a single scoring dispute.

The merge is the deploy

03

1,553 unit tests, 48 end-to-end specs, and 73 idempotent migrations. Merging to main applies migrations and ships to production; smoke tests then run against the live site. Documentation drift fails the build: change a subsystem without updating its architecture doc and CI stops the PR.

Instrumented to the fiber

04

114 documented analytics events with PII stripped at the source, tagged exception capture on every error path, and live-debugging playbooks that can read React’s internal fiber state in a running production tab. When something got weird, I could name it in minutes.

12 languages for five cents a PR

05

Locale files with 1,947 keys each, in 12 languages, kept in sync by an incremental machine-translation pipeline that only translates missing keys: about five cents and under a minute per PR, with key parity enforced in CI.

An AI news desk for $1.07

06

A daily AI-written digest and curated headlines from a 22-publisher allowlist, refreshed hourly by cron. Total model cost for the entire tournament: $1.07.

The system

One person operating all of it, with CI as the second pair of hands

ClientsInstallable PWAiOS + Android + web · offline shell12 languages1,947 keys per locale · CI parityShare cardsedge-rendered OG · signed tokensApp platformNext.js App Router on Vercel47 routes · SSR + ISR · 8 cron jobsLive results pipelinescores ingested every minute · knockout cascadeData + servicesSupabase PostgresRLS forced on 21 tables · SECURITY DEFINER writesStripe + Resendpayments · emailPostHog + Sentry114 events · tagged errorsDelivery loopmerge → migrations apply → deploy → production smoke tests1,553 unit tests · 48 e2e specs · 148 adversarial security probes in CI

Chapter 3 · Marketing · Data

Launch week: the wave

Growth was engineered around one moment: the bracket lock at first kickoff, June 11 at 19:00 UTC. A five-email countdown drip anchored to that deadline, invite links that unfurled into a league preview, and a printable schedule people could stick on the fridge all pointed at the same date. 60% of all players joined in kickoff week.

The deadline did the selling. Over the final day before the lock, completed brackets surged from 94 to 146, up 55%, with the last-call email converting 32% of the players it caught mid-bracket into finishers. The last bracket was completed eight minutes before kickoff. Across the whole campaign the system delivered 1,034 emails with zero spam complaints.

The 15-minute budget

The product’s depth is also its steepest ask. Footy26 wants an exact scoreline for all 104 matches: that is what makes every match matter and every leaderboard swing exciting. But in the expanded 48-team format, it is a real demand on a passerby’s attention. I treated about fifteen minutes as the ceiling a curious visitor would give the game, and spent design effort against it: two taps per pick with an empty save defaulting to 1-0 (visibly hinted), auto-save in the visual editor, and resume-anywhere progress.

New players per week

Accounts / UTC week

Apr 27May 4May 11May 18May 2552Jun 1165Jun 8Jun 15Jun 22Jun 29Kickoff week

60% of all players joined in kickoff week. The lock at first kickoff was the single event the whole growth motion pointed at.

From first visit to a finished bracket

Analytics + production DB

Unique visitors
611
Created an account
273
Built a bracket
181
Finished all 104 picks
146

45% of visitors created an account. 80% of bracket builders went the full 104 picks: once people started, they finished.

Google auditioned the site, then kickoff came

Search impressions / day · Search Console

1,3622,723Kickoff day · 2,723Jun 11May 18Jun 1Jun 11Jul 1Aug 1

Google surfaced the schedule, group, and team pages more each day through the run-up: 17,495 impressions peaking at 2,723 on kickoff day, with queries in a dozen languages and 10 of 12 locales earning clicks (a third landed on non-English pages). Once play began, Google's own live coverage took the results page, and what remained was brand and long-tail demand: 167 clicks in total, with the site ranking #1 for its own name.

Chapter 4 · Insights · Data · Engineering

39 days live: operating a heartbeat

From kickoff to the Final, Footy26 ran on the tournament clock: scores ingested every minute, leaderboards moving during matches, the knockout tree resolving round by round against 146 frozen brackets. Sessions averaged nine and a half minutes at a 22.5% bounce rate, and the spectator surfaces out-drew the editor: more players watched league standings move than ever touched their own bracket again after the lock.

Stoppage time on day two

Operating started immediately. Two days in, some dashboards were hanging on their loading skeletons: a rare race in the framework’s streaming hydration that only live-traffic timing could trigger. The response became the playbook. Bounded self-healing shipped before the root cause was known, turning a stuck page into a slow one; live probes then read React’s internal fiber state inside stalled production tabs until the race had a name; and every closed defect left a guard test in CI so the class cannot recur. Contained in 48 hours, cured for good by a framework upgrade weeks later, and the diagnostic playbooks are now part of the repo.

Shipped on signal

The live window is where instrumentation earned its keep. Every user-facing flow emits an event, so when behavior surprised me, the product changed while the tournament was still running:

The invisible last step

01
  1. Signal

    Players finished all 104 match picks, watched three of four bracket cards flip to "Complete", and stopped, never discovering the three bonus picks.

  2. Shipped

    A prompt at the exact moment the 104th pick saves, a bonus status card on the summary, and a persistent "All 3 bonus picks saved ✓" state replacing a dead grey button.

  3. Result

    The prompt converted 87% of the players it reached. Bonus completion (85% of builders) ended ABOVE full-bracket completion.

As shipped

The bonus status card tracking all three end-of-tournament calls with live verdicts

Picks at a glance

02
  1. Signal

    The #1 in-tournament question was "who did I pick, and what scoreline?", costing a three-tap trip to the bracket, repeated several times a day. Then a player asked to see leaguemates’ picks the same way.

  2. Shipped

    A "Your picks today" digest at the top of the dashboard, then leaguemate pick grids and tap-to-compare, two days later.

  3. Result

    114 players used the digest (4,769 views); league standings stayed the most-fired event in the app.

As shipped

Daily Kick

03
  1. Signal

    Late joiners faced a 15-day dead zone between the lock and the Second Chance bracket, with nothing to do daily.

  2. Shipped

    A one-tap daily poll with an animated global and league reveal, plus public archive pages. Works for every segment: leaders, busted brackets, and brand-new arrivals.

  3. Result

    299 votes from 81 players across the run, on top of its SEO surface.

As shipped

Second Chance

04
  1. Signal

    A real cohort arrived after the lock with no bracket to play, and busted veterans needed a reason to stay.

  2. Shipped

    A knockout-only bracket seeded from the real 32 qualifiers: one perfectly fair board where late joiners and veterans pick with identical information.

  3. Result

    101 players entered, and the app stayed alive for its newest users.

The network effect, by construction

The social loop was designed, not hoped for. Every new account auto-joins a global league, so nobody ever sees an empty leaderboard. Invite links unfurl into a league preview and complete the join after sign-in. The result: 80% of players joined a friend league (median size 5, largest 27), and 7 in 10 players arrived in a league someone else created. The product’s best growth channel was a friend with a bracket.

Built at a desk, played in a pocket

My raw analytics read roughly half desktop, but that desktop share was mostly me: filter out my own operating sessions and dev traffic, and players were three-quarters mobile before kickoff and 86% mobile across 32,500 tournament pageviews. The desktop experience I polished at my desk was, in effect, built for an audience of one. The tip jar taught the lesson best: it lived in a desktop sidebar, so most players never saw it. A mobile-visible tip tile shipped mid-tournament, and design for the pocket first became the standing rule for every surface that followed.

The tournament heartbeat

Daily active users · Jun 1 – Jul 25

65129194258Deadline eve · 258Jun 10KickoffJun 11Second Chance opensJun 28The FinalJul 19Jun 1Jun 15Jul 1Jul 19Jul 25

Deadline pressure beat the football itself: the biggest day was the eve of the bracket lock, not kickoff. The Second Chance bracket and the knockouts brought the second wave, the Final lifted engagement 2.2x day-over-day, and then a point-in-time product ended on schedule.

leagues · median size 5, largest 27
41
entered the Second Chance bracket
101
daily-poll votes across the run
299
of schedule visitors downloaded the wall planner
56%

Did they come back?

Kickoff cohort · n=414

100%Wk 022.7%Wk 119.1%Wk 222%Wk 315.9%Wk 413%Wk 59.9%Wk 6

Retention held nearly flat from week 1 through the Final instead of decaying: the tournament itself was the retention hook, and one in ten of the kickoff wave was still returning in the last week.

Where visitors came from

First-touch channel

Direct
52.8%
Social (mostly Reddit)
17.5%
Organic search
15.8%
Referral
10.9%
AI assistants
1.7%
Email
1%

Reddit alone out-drew Google search, word of mouth carried the rest, and AI assistants (ChatGPT, Perplexity) showed up as a real 2026 channel. One UTM code traced back to a paper flyer in an apartment building.

Where the game was played

Player pageviews by device, my sessions excluded

May, pre-kickoff

Mobile 74.9%
Desktop 25.1%

Live, Jun 1 - Jul 25

Mobile 86%
Desktop 13.8%

Players were a phone audience from the first week and only more so once the tournament went live. The desktop share in the raw analytics was mostly my own operating sessions, and the tip jar in a desktop sidebar proved which surface mattered.

Chapter 5 · Insights · Marketing

The Final, and the VAR review in 24 hours

Spain beat Argentina 1-0 on July 19. Of the 146 complete brackets, 43 had called the champion from day one, a 29.5% hit rate, while 60.3% watched their pick go out before the Final. The global leaderboard was decided by 55 points, 756 to 701.

The tournament’s emotional peak is a 24-hour window, so the send had to land that night. "Footy26 VAR: Tournament Review", the per-player season recap covering placement, accuracy, round-by-round scoring, and bonus verdicts, went from approved spec to production sends in about 24 hours with zero new database migrations. Six pull requests shipped on Final day itself, and the thank-you edition reached all 177 reachable bracket builders that evening. Players who reviewed the season rated it 4.5 out of 5.

Who we crowned before a ball was kicked

146 complete brackets

ESP
43■ Champion
FRA
30
BRA
20
ARG
15
POR
11
ENG
10
GER
6
USA
4
Other
7

43 brackets (29.5%) called Spain from day one. 60.3% saw their champion eliminated before the Final ever kicked off, which is exactly what made the leaderboard worth watching.

of complete brackets called Spain
29.5%
decided the global leaderboard
55 pts
from Wrapped spec to production sends
~24 hrs
season rating from player reviews
4.5/5

Extra time

The moments worth keeping

A bracket game lives or dies on its moments. These are the ones the product staged on purpose, captured as they shipped.

Kickoff

01

When the tournament went live, the whole app paused for a 3-2-1 countdown, a confetti drop, and a personal strip: your champion, your sealed picks, your leagues. Shown exactly once per player.

Second Chance opens

02
The Second Chance opening sheet: "Your Second Chance is open", with a lock countdown and Make your picks button

June 28: busted brackets and late arrivals got a fresh knockout board seeded from the real 32 qualifiers, announced with a one-time sheet counting down to its own lock. 101 players entered.

Footy26 VAR: Tournament Review

03

Within 24 hours of the Final, every builder got a personal full-time review: where they finished, how sharp they were round by round, and a verdict on their champion call. Players rated the season 4.5 out of 5.

Share cards

04
A generated match-call share card: "I called it.", Brazil 2-0 Ecuador with an exact-score chip and +20 points banked

Five card kinds in three formats each, rendered server-side per player: brag, underdog, rivalry, champion, and the match call, the one players reached for most. Nearly every card went out in the square format, straight to the group chat, and league invite links (shared 163 times) did the heaviest social lifting.

The penalty shootout 404

05

A canvas penalty mini game standing where an error page should be: drag the ball, pick your corner, and the keeper sharpens as your streak climbs. 83 shootouts from 38 players, 15 of them started from a wrong URL. Still live.

Take your penalties

Chapter 6 · Entrepreneurship

What it becomes

The sharpest thing the season taught me: I did not build a World Cup app. I built a social-prediction platform and shipped a World Cup app as its first tenant. The auth, leagues, invites, scoring rails, email machine, observability, design tokens, and 12-language chassis are sport-agnostic, and the privacy model was deliberately built so a second competition cannot cut corners: shared data stays counts-not-values, and every new competition must declare its own visibility rule.

The players saw it too: every season-review respondent independently asked for tennis and NBA brackets next. The architecture work is specced, with rolling per-round lock windows replacing the single global lock so a season stays alive for late joiners, and the 2027 Women’s World Cup, same sport, same flags, same audience, is close to a configuration change. The bracket is the beginning.

The demo is still on the pitch

Footy26 stays live as a playable demo, and the platform it proved out is what I am building next. If you want product built with this kind of care, from strategy to shipped and operated, let's talk.

Role
Solo: product, design, full stack, growth, ops
Timeline
First commit May 1 · Final Jul 19, 2026
Stack
Next.js · Supabase · Vercel
Status
Tournament complete · playable demo live

Every figure on this page is an exact production number, pulled 2026-08-11 UTC (traffic, device, poll, share, and penalty figures re-verified 2026-08-19 UTC) from the live Postgres database, PostHog analytics, and Google Search Console (export window May 18 to Aug 17, 2026), cross-checked against dated snapshots taken during the tournament. Counts of people use the database as ground truth; behavior and traffic use analytics (consenting users only, so behavioral counts are floors, not estimates). Traffic figures count pageviews on footy26.com only, excluding localhost and preview-deploy noise, and device figures also exclude my own operating sessions where stated. The fifteen-minute attention budget is a stated design assumption, not a measurement. Product captures come from the Footy26 demo environment with simulated players: the privacy model shares counts, never a real player’s picks, and that applies to this page too.