DEV Community

Juan Camilo Chacón A
Juan Camilo Chacón A

Posted on

I built a free football prediction platform for World Cup 2026 — here's the stack

With the FIFA World Cup 2026 just months away, I wanted to build something my friends and I could actually use — a platform to create prediction pools (known as quiniela in Mexico, polla in Colombia, prode in Argentina, or penca in Uruguay).

The result is Picks4All — a free, open platform where you can create a pool, invite friends with a code, predict match scores, and compete on a live leaderboard.

Here's how I built it and what I learned along the way.

Landing page

The Stack

Layer Technology
Frontend Next.js 16 (App Router), React 19, TypeScript
Backend Node.js 22, Express, TypeScript
Database PostgreSQL 16, Prisma ORM
Auth JWT + Google Sign-In
i18n next-intl (ES/EN/PT)
Email Resend
Deployment Railway

I chose a monorepo with a clear separation: backend/ for the Express API and frontend-next/ for the Next.js app. Both are TypeScript end-to-end.

Key Architecture Decisions

Template → Instance → Pool

The most important design decision was the data model. Instead of hardcoding tournaments, I built a template system:

  • Template: defines the tournament structure (teams, groups, phases, matches)
  • Instance: a playable edition of a template (e.g., "Champions League 2025-26")
  • Pool: a group of friends competing on an instance with their own scoring rules

This means adding a new tournament is just creating a new template — no code changes needed.

Smart Sync — Automatic Score Updates

Nobody wants to manually enter scores for 64 matches. I integrated with API-Football to build a "Smart Sync" system:

  1. A cron job checks for live/finished matches every minute
  2. When a match finishes, it fetches the score and publishes the result
  3. The leaderboard updates automatically

The tricky part was handling edge cases: delayed matches, score corrections, and rate limits on the free API tier (100 requests/day).

i18n with next-intl

The app supports Spanish, English, and Portuguese. I used next-intl v4 with a localePrefix: 'as-needed' strategy:

  • picks4all.com/ → Spanish (default, no prefix)
  • picks4all.com/en/ → English
  • picks4all.com/pt/ → Portuguese

Each locale has its own SEO metadata, JSON-LD structured data, Open Graph images, and sitemap entries. This was more work than I expected, but it's essential for reaching users across Latin America, Spain, Brazil, and English-speaking countries.

What the App Looks Like

Pool page with matches and predictions

Inside a pool you can:

  • Browse matches by phase (group stage, round of 16, etc.)
  • Submit your score predictions before the deadline
  • See official results with scoring breakdowns
  • Track your position on the leaderboard

Deployment

Everything runs on Railway — three services:

  1. Backend (Express API)
  2. Frontend (Next.js standalone)
  3. PostgreSQL database

DNS is on Cloudflare, pointing picks4all.com to the frontend and api.picks4all.com to the backend. Total cost is under $10/month.

Lessons Learned

  1. Start with the data model. I spent more time on the template/instance/pool schema than on any UI component, and it paid off — adding Champions League after World Cup took hours, not days.

  2. i18n is never "just translations." It touches routing, SEO, metadata, legal pages, URL structure, and even date formatting. Plan for it early or pay the price later.

  3. Rate limits are a feature, not a bug. The API-Football free tier forced me to build a smarter sync system — checking only active matches, batching requests, caching results. The paid tier would have let me be lazy.

  4. Ship early, iterate with real users. My friends found UX issues in the first 10 minutes that I never would have caught alone.

Try It / Check the Code

World Cup 2026 is coming. If you've ever organized a quiniela on a spreadsheet, give this a try. It's free, no ads, no gambling — just friendly competition.


Questions about the architecture or stack? Drop a comment — happy to go deeper on any part of it.

Top comments (0)