Back to projects

Case study

Escala Aí

Live demo

A Brasileirão Série A fantasy game with a hard rule — one player per position per round, never reused in the half-season. Lineups lock at first kickoff; scoring and ranking read our database, not the sports-data provider.

Introduction

Escala Aí is fantasy football with a constraint that makes every pick expensive: GOL, LAT, ZAG, MEI, ATA — one player each, and you cannot play that player again in the turn. The market stays open until the first match of the round; then the lineup freezes. I designed the product rules, the pitch UI, and the data path so the app never calls SportMonks on a user request — Vercel crons enqueue, Inngest workers pull finished fixtures into Postgres, and scoring is a pure function we can test.

  • Pitch + squad list with per-player scores and round lock state
  • Market open / locked tied to the first fixture of the round
  • One pick per position; no player repeats for the same user in the half-season
  • Leagues (public or private) and ranking snapshots — not live partial scores
  • SportMonks → Inngest → our database; ~400 API calls/month budgeted on purpose
  • Cartola-style scout table (saves, clean sheets, goals conceded for GOL/ZAG) with isolated tests

Role — Solo architect and developer: rules, ingestion pipeline, scoring, ranking, PWA, and deploy. Design follows the Dupla brand (neon on ink, pitch as the home screen).

Technologies — Next.js, TypeScript, tRPC, Supabase, Inngest, Tailwind CSS, Vercel — Hosted on Vercel; Postgres and auth via Supabase; background jobs on Inngest.

Screenshot of Escala Aí

Purpose and Goal

The product exists to give Dupla’s audience a fantasy loop they already understand — pick a team, wait for the round, climb a ranking — with a rule set that is fair and unambiguous.

For me the outcome was an app that can survive a Brasileirão weekend: fixtures finish at night, scores must land, and we cannot burn the SportMonks quota on live polling.

Design started from the constraint. If the provider is budgeted at a few hundred calls a month, the app must read our tables. Crons only enqueue; workers only fetch a fixture when the database says it is finished and still unscored. Ranking is a snapshot after the round, not a moving target mid-match.

Spotlight

Every round hangs on two promises: your five names are locked at first kickoff, and the points you see match the scout table — not a guess from a live widget. The hard part is the ingest budget. SportMonks is metered; “realtime” would lie. I pushed all provider I/O behind Inngest steps with a gate in Postgres (already scored → skip), kept scoring as pure functions so GOL-only saves and ZAG/GOL goals-conceded stay honest, and let tRPC procedures own picks and eligibility so a refresh cannot sneak a sixth player onto the pitch. The UI is a campinho because that is how people think about a lineup; the server is where “escolheu, não repete” is actually enforced.

Lessons Learned

  • Treat a third-party sports API as a budget, not a socket. Enqueue, gate, then fetch.
  • Fantasy rules belong on the server — lock time, position caps, and no-repeat are eligibility, not CSS.
  • A scout table the business can read (and tests can pin) beats magic numbers in the UI.
  • No live partial score is a product decision: wrong points during a match destroy trust faster than a dash in the cell.