Async trivia, end-to-end
How a turn becomes points, how cards stay fair, and how the platform scales from football to any sport.
System overview
┌────────────┐ ┌────────────┐ ┌────────────────┐ ┌──────────────┐
│ Mobile │───▶│ Edge API │───▶│ Match engine │───▶│ Postgres │
│ (Flutter) │ │ (TanStack) │ │ Turn / Card │ │ (RLS, Auth) │
└─────┬──────┘ └──────┬─────┘ └────────┬───────┘ └──────┬───────┘
│ │ │ │
│ ┌─────▼──────┐ ┌──────▼──────┐ ┌──────▼───────┐
└───────────▶│ FCM / APNs │◀────│ Rules eng. │ │ Cards / IDX │
│ Push │ │ validate() │ │ pg_trgm │
└────────────┘ └─────────────┘ └──────────────┘
Single binary for iOS + Android. Optimistic local turn UI with reconciliation on server response.
Stateless serverless. Validates payloads, applies rate limits, calls match engine.
Pure functions: nextClue(), submitAnswer(), scoreTurn(). 100% unit-tested, deterministic.
Sport-agnostic schema with `sport` column. Row-level security keeps users scoped.
Turn change events fan out via webhooks; quiet hours respected per user.
Reports + rules engine + admin review keep the catalog clean.
Turn flow (sandwich structure)
- 1. Card drawnEngine picks next card from match queue (10 per match).
- 2. Clue cyclePlayer A receives clues 1–6. Each clue advances on tap or after 20–25s timer.
- 3. AnswerEngine validates against `aliases[]` with accent + case insensitive match.
- 4. ScorePoints = 7 − clueIndex (cap 6, min 1). Wrong = 0.
- 5. SwapPlayer B faces the same card. Asymmetric info — only knowledge of B's own progress.
- 6. NotifyFCM/APNs dispatch on turn flip; deep-link opens directly to the card.
- 7. TiebreakerIf tied at card 10 → bonus card. If still tied → fastest millisecond response wins.
- 8. RecapBoth players see final scores + correct answers, with single ad on results screen.
Rules engine
┌──────────────────┐
answer ───▶│ normalize() │── strip accents, lowercase, trim
└────────┬─────────┘
▼
┌──────────────────┐
│ alias match? │── exact match against card.aliases[]
└────────┬─────────┘
yes ─────┘ └──── no ───▶ fuzzy match (Levenshtein ≤ 2) ?
│ │
▼ ▼
score = 7 − clueIdx accept or reject
│ │
└───────────────┬───────────────────┘
▼
persist match_turn row
Data model (simplified)
id, username, email, country, created_at, premium
id, user_a, user_b, status, created_at
id, sport, category, answer, aliases[], clues[], difficulty, status
id, sport, player_a, player_b, status, mode, started_at
id, match_id, card_id, player, answer, score_ms, points
id, day, user_id, points, time_ms