← Back to Work

Balalino — Fantasy World Cup (freelance)

Production fantasy World Cup web app: phone-OTP auth, redeemable codes, squad transfers, admin tooling for seasons and players. Freelance engagement, full-stack across frontend and backend on Next.js 16 + Supabase, built with one collaborating developer; design was provided.

Role
Freelance — Senior Frontend Engineer (full-stack · 2-developer team · design provided)
Published
فبراير ٢٠٢٦
Roles
2
Route Handlers
30+
Auth
Password + OTP
Next.js 16React 19TypeScriptTailwind CSS v4SupabaseZodReact Hook FormRechartsTwilio (OTP)ZustandPlaywrightVitest
View live site →

Context

A fantasy World Cup web app: end users register, claim a redeem code, build a squad from a pool of players within a season's budget, and submit transfers as the tournament progresses. A separate admin surface manages seasons, teams, players, redeem-code batches, contact messages, and analytics.

Engagement shape. Freelance build delivered with one other developer alongside me; design and product copy were provided by the client. I owned the architecture, the full-stack scope (Next.js App Router + Route Handlers + Supabase schema and RLS), the admin tooling, the test setup, and code review — the second engineer worked against the patterns and boundaries set up here.

The product is split into clearly-scoped route groups — (marketing) for the public landing, (auth) for end-user sign-in / OTP / password reset, (admin-auth) for staff sign-in, (user) for the gameplay surface (collection, teams, transfers, redeem, profile), and admin/* for the back office.

Problem

Fantasy games look like CRUD until you actually build them. The constraints that shape the architecture:

Architecture

Why two auth strategies, one session?

Mixing OTP and password into a single sign-in flow creates UX and security regressions on both sides. Separate Route Handlers (/api/auth/login, /api/auth/verify-otp, /api/auth/admin/login) keep the flows distinct, but each writes to the same httpOnly cookie shape so downstream handlers don't care which path produced the session.

Implementation

Schema validation everywhere. Zod schemas back every Route Handler and every form via @hookform/resolvers, so the client and server share types — no drift between what the form accepts and what the API will store.

Squad and transfers. Squad construction goes through /api/user/teams/[teamId]/squad, which validates budget, position counts, and transfer-window state on the server before persisting. The client UI prevents most invalid states for UX, but the server is authoritative.

Redeem codes. Generated server-side in batches, hashed at rest, single-use; admins manage batches via /api/admin/redeem-code-batches. The user-facing endpoint (/api/redeem) checks redemption window, idempotency, and a per-user rate ceiling before unlocking the redeemed entitlement.

Admin analytics. /api/admin/analytics returns aggregated metrics that the admin dashboard renders with Recharts — registrations, OTP success rate, contact-message volume, redeem usage by batch.

Testing. Domain helpers and validation rules are unit-tested with Vitest; user-facing flows (register → OTP → redeem → build squad → transfer) are covered by Playwright specs.

Outcome

← All case studies