Architecture
For a developer taking over maintenance of iRank.
Shape of the system
| Layer | Technology |
|---|---|
| Frontend | Next.js (App Router), React, Tailwind, shadcn/ui |
| Backend | Convex — database, queries, mutations, actions, scheduling |
| Hosting | Vercel (frontend), Convex Cloud (backend) |
| SMTP via Nodemailer, in Convex’s Node runtime | |
| Push | Web Push (VAPID) |
The app is effectively a single-page application: almost every component is a client component, and data comes from Convex’s live queries rather than from server rendering.
Where the important logic lives
| Concern | Location |
|---|---|
| WSDC scoring rules | lib/scoring/wsdc.ts |
| Pairing algorithm | lib/pairing/ |
| Ranking model | lib/ranking/ |
| Offline store and outbox | lib/offline/ |
| Ballot validation | convex/lib/ballot_validation.ts |
| Panel reconciliation | convex/lib/ballot_results.ts |
| Release gating | convex/lib/ranking_release.ts |
lib/scoring/wsdc.ts is the single source of truth for scoring. Both the
browser and the server import it, so a rule cannot be enforced differently in
two places.
The pairing algorithm is isomorphic
One pure module runs in both the browser and the server. It has no Convex or
React imports, takes explicit inputs, and is deterministic: a seeded PRNG, no
Date.now(), and total sort orders that end on a stable identifier.
That determinism is load-bearing. A device can pair a round offline, and on sync the server re-runs the identical algorithm over its own data and compares the result. A mismatch is rejected.
Offline
Convex’s own mutation queue is in memory and does not survive a reload. The
durable outbox in lib/offline/ is separate: Dexie over IndexedDB, with
idempotency keys, retry with backoff, and replay on reconnect.
Anything computed on a device is re-verified on sync. The client is never the authority.
Background work
Convex components carry the asynchronous load:
| Component | Used for |
|---|---|
| Workpool | Bulk email, push notifications, ranking rebuild fan-out |
| Action cache | Gemini calls, keyed on content rather than session |
| Action retrier | Transactional email — invitations, magic links |
Two workpools exist: notificationPool (6 in flight) and rankingPool (2).
They are separate so a nightly ranking rebuild cannot delay a motion release.
Scheduled jobs
Defined in convex/crons.ts. The important one is the nightly ranking snapshot
rebuild, which fans out per tournament through rankingPool and merges when the
last tally reports in.
Testing
Vitest across three projects — Convex functions in an edge-like runtime, pure modules in Node, components in jsdom — plus Playwright for mobile layout at 360px. Property-based tests cover pairing at field sizes from 2 to 400 teams.