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.
Enterprise access
Tournaments are private owned resources by default. Creation accepts
--workspace-id and --visibility private|workspace; the shared access API uses
tournaments with reader/editor/manager grants. Participants, matches, standings,
lists and profile includes inherit the tournament's read boundary.
Readers may register themselves while registration is open. Editors may start or complete the tournament. A shared editor still cannot report a match unless they are one of its actual participants. Reporting also checks that the match belongs to the tournament in the URL. Membership, workflow state and results cannot be forged through an ordinary scoped database client.
Seeding from a leaderboard requires independent read access to that namespace, checked at creation and again at start. Sharing a tournament does not share its seeding source. Bracket formats, capacity checks, byes, score validation and progression retain their existing business rules.
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: 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:
- 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",
"page": { "has_more": false }
}Standings, participants, and matches are
lists: page them with --limit and
--cursor; participants filter on user_id, checked_in, eliminated,
and their record, matches on round_number, status, the participants,
and winner_id.
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 takes the
shared list parameters: q, filter on
format, status, start_time, winner_id, created_at and updated_at,
and sort by name, start_time or created_at (the default, newest
first), as in ?q=docs4&filter[status][eq]=completed. The CLI's
tournament list takes the same options (--filter 'status[eq]=completed').
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.
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
