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.
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) |
| 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:
- A cron job checks for live/finished matches every minute
- When a match finishes, it fetches the score and publishes the result
- 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
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:
- Backend (Express API)
- Frontend (Next.js standalone)
- 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
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.
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.
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.
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
- Live app: picks4all.com
- Source code: github.com/Rastipunk/Quiniela-Platform
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)