Tournament
Bracket competitions from registration to a crowned champion: four formats (single elimination, double elimination, round robin, swiss), participant self-registration, generated brackets with byes, player-reported scores, automatic round progression, and live standings.
When to reach for it: game tournaments, playoff events, community competitions, hackathon brackets - any fixed field of entrants that plays down to a winner.
When not to: a continuously updated ranking over an open-ended metric belongs to Leaderboard; pairing players of similar skill into ad-hoc matches belongs to Matchmaking.
Concepts
- Lifecycle -
draft->in_progress->completed. Registration is open only before start,startgenerates the bracket and cannot be undone, and the tournament completes itself when the last match resolves. - Formats -
single_elimination,double_elimination,round_robin(every pairing generated up front),swiss(a fixed number of rounds, each paired by score once the previous round finishes).formatserializes as a plain string except swiss, which is{"swiss": {"rounds": N}}. - Participants are the callers -
registerenrolls the JWT subject; there is no flag to enter someone else, and each user can register once per tournament. Matches and standings reference participant IDs, not user IDs. - Matches carry
round_numberandmatch_number. Reporting a result fills or creates the next round automatically; a match becomesreadyonce both slots are occupied. Double elimination numbers its losers bracket from round 1001. - Scores are reported by the players - the caller must be one of the match's two participants (either one, loser included), the declared winner must be in that match, and a match is reported exactly once.
- Byes - with an odd field, the leftover participant's match is stored
already
completedwith score"Bye"and counts as a win.
Create and register
snug tournament create --name "docs4-cup" --format single_elimination --max-participants 4
snug tournament register <tournament-id> # enrolls the calling user
snug tournament participants <tournament-id>
snug tournament info <tournament-id>Over HTTP, creation is POST /api/v1/tournaments with tournament_type
and an optional config object - the only way to set custom swiss rounds
(the CLI's --format swiss defaults to 6):
curl -X POST -H "Authorization: Bearer $SNUG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "docs4-http-open", "tournament_type": "swiss",
"config": {"swiss_rounds": 3, "max_participants": 8}}' \
http://localhost:4000/api/v1/tournaments{
"status": 201,
"msg": "Created",
"data": {
"id": "eqdNhpaHdcJADHpEQsDF",
"name": "docs4-http-open",
"format": { "swiss": { "rounds": 3 } },
"status": "draft",
"max_participants": 8,
"winner_id": null,
...
}
}Registration refusals, each reproduced live: a second register by the
same user is 409 unique_constraint_violation, registering at capacity is
400 tournament_full, and registering after start is
400 registration_closed. An unknown tournament_type at creation is
400 invalid_format.
Start and the bracket
snug tournament start <tournament-id>
snug tournament matches <tournament-id>start flips the tournament to in_progress and returns the opening
round - for a 4-player single elimination, two pending matches. Starting
again (or after completion) is 409 tournament_already_started; starting
with zero participants is 400 insufficient_participants. Verified with
three players: match 2 came back already completed with score "Bye"
and its lone participant advanced. Participants carry a seed_rank that
bracket generation would sort by, but no endpoint sets it today - everyone
registers unseeded, so round 1 pairing simply follows the participant
listing.
GET /api/v1/tournaments/{id}?include=participants,matches embeds both
rosters in one response.
Report scores
snug tournament report-score <tournament-id> <match-id> \
--winner <participant-id> --score "2-1"--winner is a participant ID, --score a freeform string (max 100
chars). Refusals, each reproduced live:
- Caller not in the match:
403 unauthorized_reporter. --winnernot in the match:400 winner_not_in_match(checked before the caller's own authorization).- Match already reported:
409 match_already_completed- results are final, there is no re-report or dispute endpoint. - Tournament not
in_progress:400 tournament_not_in_progress.
Progression is automatic: after both round-1 reports in the verified
4-player single elimination, matches showed a round-2 final in status
ready holding the two winners. In swiss, only round 1 exists at start;
completing it generated round 2 paired by score - winners against winners.
Standings and finishing
snug tournament standings <tournament-id>
snug tournament complete <tournament-id>Standings rank by wins; captured mid-tournament:
{
"standings": [
{ "rank": 1, "participant_id": "jXuBQ...", "user_id": "docs4-dave",
"wins": 1, "losses": 0, "points": 3.0 },
...
],
"tournament_id": "aAJrExyTZuFxjueAZtWJ"
}points changes meaning by format: 3 per win in elimination and round
robin, but in swiss it is the Buchholz tiebreaker (sum of your opponents'
wins) - a swiss winner can show fewer points than the players they beat.
Reporting the last match finishes the tournament on its own: the verified
final report flipped status to completed and set winner_id with no
further call. complete exists to end a tournament early - it is
idempotent on a completed tournament, and in swiss and round robin it
crowns the current standings leader (verified after swiss round 1 of 3).
In elimination formats an early complete takes the winner of the
latest-round match, so with an unplayed final it records winner_id: null - report all matches instead.
Finding tournaments
GET /api/v1/tournaments supports the
shared search grammar (q, filter,
sort_by, sort_order, page) plus a status shortcut - verified with
?q=docs4&status=completed. The CLI's tournament list takes no filter
flags; filtered queries go over HTTP. There is no delete endpoint - once
created, a tournament stays.
Limits and configuration
Tournaments accept up to 1024 participants by default, tunable via
TOURNAMENT_MAX_PARTICIPANTS_PER_TOURNAMENT in the
Tournament CONFIG reference.
Per-tournament caps below that are set at creation with
--max-participants.
Reference
- Tournament API - every endpoint, callable
- Related: Leaderboard for continuous rankings, Matchmaking for skill-based pairing