ServicesTournament

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, start generates 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). format serializes as a plain string except swiss, which is {"swiss": {"rounds": N}}.
  • Participants are the callers - register enrolls 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_number and match_number. Reporting a result fills or creates the next round automatically; a match becomes ready once 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 completed with 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.
  • --winner not 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

On this page