Full-stack case study
Tournwa
The backend powering Tournwa, a UAE sports platform — a four-format competition engine from seeding to league rankings, Stripe Connect entry fees, and registration for athletes solo, as teams or as doubles partners.
- Role
- Lead Backend Developer
- Duration
- 8 months — ongoing
- Year
- 2026
- Stack
- Node.js · Express.js · PostgreSQL · Redis · Stripe
- Live
- tournnwa.com

01 / The Problem
Real competition needs more than a signup form.
Organizers running structured sports competitions need categories, seeding, group stages, knockout brackets, per-set scoring and league points — plus entry fees, approvals, capacity limits and parent consent for minors. Athletes need to find events and register solo, as a team, or with a doubles partner. No spreadsheet survives that.
Target users
Three roles: organizers — clubs and academies running tournaments, trainings and pickup games; athletes, including minors behind parent-consent flows; and platform admins.
02 / The Solution
One backend, three apps.
A single Express API serving three surfaces — an athlete mobile app, an organizer web app and an admin panel — through 338 endpoints across 31 route groups. The competition engine carries a category from seeding through groups and brackets to point settlement; Stripe Connect routes entry fees to premium organizers with the platform's per-event fee taken automatically; and five background queues keep SMS, email and push notifications out of the request path.
Why this stack
- PostgreSQL — partial unique indexes, row locks and transactions guard money
- Redis — cache, JWT blacklist, OTP store and rate limits in one piece
- BullMQ — five queues keep Twilio, SMTP and Expo push off the request path
- Node cluster — multi-core workers with connection pools budgeted per worker
03 / Features
What the platform does.
Auth, Four Ways
Email, Google, Apple and phone OTP with a Redis token blacklist
Competition Engine
Seeding, groups, brackets, per-set scoring and tie-broken standings
Stripe Connect
Destination charges with per-event platform fees and payouts
Flexible Registration
Solo, team or doubles — with parent consent gating for minors
League Points
Placement-to-points models settled into an append-only ledger
Organizer Spaces
Private communities that never leak into public listings
04 / Technical Challenges
The hard parts — and how they fell.
A four-format competition engine
Single elimination, single- and multi-group round robin, and two-stage groups-to-knockout — with byes, seeding methods, third-place matches, and rules that must freeze the moment play starts.
How I solved it
Brackets pad to the next power of two, order seeds with a recursive 1-vs-N mirror and pre-wire winner/loser edges, so completing a match propagates results mechanically; byes auto-complete at generation. Groups distribute serpentine-style and schedule by the circle method. Guards enforce forward-only match statuses and freeze competition settings once seeding exists — every mutation transactional, cache invalidation deferred to after commit.
Race-proof payments
Finalization can arrive twice — webhook and client poll — users double-submit intents, and a payment must never double-approve a registration or double-reserve a capacity slot.
How I solved it
Two partial unique indexes allow exactly one active and one completed payment per registration; finalization takes a row lock, early-returns if already done, then approves the registration, reserves the slot and activates participants in one transaction. Stripe idempotency keys derive from internal IDs, and receipt emails are double-guarded by deduped queue job IDs plus a null-check update.
Points that settle exactly once
When a tournament completes, placements must convert to league points exactly once per category even on retries — and stay correctable when organizers later edit points models.
How I solved it
Settlement derives placements per format and skips any category that already has a ledger entry for the tournament. Corrections diff the desired totals against the ledger sum and write compensating rows — history is never edited or deleted. Leaderboards rank in SQL with tie sharing behind a version-keyed five-minute cache.
05 / Architecture
How the system fits together.
THREE CLIENT APPS
athlete mobile · organizer web · admin
EXPRESS 5 API
338 endpoints · JWT + RBAC · cluster
POSTGRESQL
~60 models · transactions · row locks
STRIPE CONNECT
entry fees & payouts
REDIS + BULLMQ
cache · OTP · five queues
TWILIO · SMTP · EXPO
sms · email · push
CLOUDFLARE R2
presigned media uploads
External services
06 / Screenshots
The product, up close.


07 / Results
What it moved in the real world.
- API endpoints
- 0
- Automated test cases
- 0
- Database models
- 0+
- Background queues
- 0
One API in production behind an athlete mobile app, an organizer web app and an admin panel. Organizers run complete competitions — seeding, groups, brackets, scoring, rankings — while entry fees settle through Stripe Connect with the platform fee taken automatically per event. The money paths and the engine's golden paths are covered by a 29-file Jest suite.
08 / Tech Stack
Built with.
Frameworks
- Node.js
- Express 5
- Sequelize
- Jest
Libraries
- BullMQ
- Stripe
- ioredis
- Joi
- Twilio
- Expo Push
Infrastructure
- PostgreSQL
- Redis
- Railway
- Cloudflare R2
- Stripe Connect
09 / My Contribution
What I built, specifically.
- Designed and built the tournament competition engine: four format combinations, seeding with bye auto-resolution, snake group distribution, circle-method scheduling and standings with recursive tiebreaks.
- Built the Stripe Connect payment system: dual payout modes, partial-unique-index and row-lock idempotency, and webhook-driven finalization that approves and reserves in one transaction.
- Built idempotent league-points settlement with an append-only correction ledger and SQL-ranked leaderboards with tie sharing.
- Built Organizer Spaces — header-scoped private communities with strict visibility isolation, verified by dedicated leak tests.
- Built the operational layer: clustered server with graceful shutdown, Redis version-key caching, five BullMQ workers including an idempotent reminder sweep, rate limiting, and the 186-case test suite. (Teammate: social-auth integration and team-management CRUD.)