Tournament formats and lifecycle
This chapter covers the two tournament formats, how groups and qualifiers and the small final work, the two team sizes (1v1 and NvM team battles), the ready-up window that takes a match live, and the lifecycle states a tournament and a match move through. Every field named here is a real column on the tournaments, matches, groups, teams, or team_members tables.
The two formats
A tournament's format is one of two values:
groups_playoffs(World Cup). A round-robin group stage followed by a single-elimination playoffs. This is the default.playoffs_only. Straight single-elimination knockout from the start, no group stage.
World Cup: groups_playoffs
Registrants are split into groups of groupSize (default 4). Within each group a full round-robin is generated: every player faces every other player once. Each match is a scheduled window [opensAt, closesAt], roundDurationHours long (default 48).
Group standings rank players by points, with tiebreakers, using the pure standings math in src/core/. The public standings row exposes played, wins, losses, scoreDiff, and points.
The top qualifiersPerGroup players from each group (default 2) advance to the playoff bracket. Qualifier seeding is computed so that group winners are spread across the bracket rather than meeting early.
Playoffs: single-elimination with a small final
The playoff bracket is single-elimination. Matches carry a stage of playoff, and the last two are final and small_final. The small final is the third-place match: the two losing semifinalists meet so the bracket resolves a clean 1st, 2nd, and 3rd. Bracket wiring is explicit on each match: feedsMatchId / feedsSlot say where the winner goes, and for semifinals loserFeedsMatchId / loserFeedsSlot route the loser into the small final. bracketSlot labels positions like R1M3, SF1, SMALL_FINAL, and FINAL; roundLabel gives the human label ("Quarterfinal 1", "Final").
The top three earn a Steam-visible tournament trophy (trophy_grants, one row per rank 1/2/3, per user so every roster member of a winning team is covered).
Match stages
matches.stage is one of:
| Stage | Meaning |
|---|---|
group |
A round-robin group match (World Cup only) |
playoff |
A knockout bracket match |
small_final |
The third-place match |
final |
The championship match |
1v1 and NvM team battles
A tournament sets a min and max players per side with teamMinPlayers and teamMaxPlayers (both default 1):
1 / 1is classic 1v1: each side is a single player.teamMaxPlayers > 1is an NvM team battle: each side is a team roster. AllowingteamMinPlayers < teamMaxPlayerssupports asymmetric or flexible NvM, where a side may field fewer players than its full roster.
The captain-as-side model
Team battles reuse all of the 1v1 bracket math with no changes. The trick is that the captain's user id doubles as the side id everywhere a match references a "player":
- A
teamsrow has acaptainUserId;team_membersrows list the roster (one team per player per tournament). - In
matches,p1UserIdandp2UserIdhold the two captains' ids, exactly where a 1v1 match holds two players. - Registrations, placements, standings, and bracket propagation all operate on the captain id as the side id, so the group and bracket engines are identical for 1v1 and NvM.
- Any roster member may act for the side. Marking ready, reporting a result, confirming, and disputing all resolve the acting user to their side's captain id, so a teammate can ready up or report on the team's behalf.
For game-verified results, team matches use the same braket.result.v3 protocol as 1v1: the game names any one player on the winning side as winner, and Braket maps that SteamID64 to a side via the registered rosters. Those rosters plus per-side limits are handed to the game via the live-match discovery endpoint (p1Roster, p2Roster, teamMinPlayers, teamMaxPlayers). See The game-client result protocol (dual attestation).
Ready-up windows: how a match goes live
A match does not go live on a timer; it goes live when both sides ready up while the window is open, FaceIt-style. This is the readiness model in src/services/tournamentService.ts.
- Each side calls ready within the match window
[opensAt, closesAt]. Readying before the window opens (opensAt) or after it closes (closesAt) is refused. - A ready is only valid while fresh: it expires after
READY_TTL_MINUTES = 10minutes unless the match already went live. So both sides must be ready within the same 10-minute freshness window. - When the second side readies and the first side's ready is still fresh, the match transitions to
live:startedAtis stamped, a per-matchsessionNonce(8 random bytes, hex) is minted, and a neutralgameSeedis issued for the game's shuffle/RNG (surfaced to the game via live-match discovery, so a player-host cannot pick a favorable deal). The nonce is part of the game-client result commitment and prevents replaying a result across matches. - After going live, the two sides play your game on Steam and then a result arrives (player-reported, game-client, or AI-video).
The readiness fields on a match are p1ReadyAt, p2ReadyAt, and startedAt; sessionNonce is null until the match goes live.
Tournament lifecycle states
tournaments.status moves through:
| Status | Meaning |
|---|---|
draft |
Being configured; not visible in the public API |
registration_open |
Players may register (up to maxPlayers) |
registration_closed |
Registration ended; group stage not yet generated |
group_stage |
Round-robin groups are being played (World Cup only) |
playoffs |
The single-elimination bracket is being played |
completed |
Podium resolved; completedAt stamped, anchored to a ladder season |
cancelled |
Called off |
Draft tournaments are hidden from the public REST API; every other status is visible.
Match lifecycle states
matches.status moves through:
| Status | Meaning |
|---|---|
scheduled |
Window assigned; not yet live |
live |
Both sides readied; the game is being played; sessionNonce minted |
awaiting_result |
A result was reported/submitted but not yet confirmed by the other side |
confirmed |
Result agreed and final |
disputed |
Conflicting reports/submissions; awaits admin resolution |
forfeited |
Resolved by forfeit (admin) |
confirmed and forfeited are the two resolved states; propagation to the next bracket match happens on resolution. How a match reaches confirmed or disputed (report, confirm, dispute, auto-confirm grace, game-client dual attestation, AI video) is the subject of Result verification: the resultSource model.
Ladder and seasons
Completed tournaments with at least 5 participants feed a season ladder: one placement row per participant per completed tournament, with points = 10 * (N - placement + 1) where N is the field size. completedAt anchors the tournament to its ladder season. Community-run "fan bracket" tournaments (isCommunity) are ladder-ineligible by default (ladderEligible), and the studio can flip either. The ladder is exposed at GET /api/v1/ladder (see Public REST API reference).