Architecture Documentation

MTG Community Rankings

A community ranking platform for Magic: The Gathering. Dual Elo per player per season, match results ingested from pickups, in-tool tournaments, Challonge and Melee, with admin-defined formats, seasons and achievements.

Clean Architecture Python API React SPA PostgreSQL OpenAPI 3.1
01 — Overview

What the system does

Players register with Discord, play matches anywhere, and see themselves climb. Results flow in from four sources into one append-only match ledger; the ledger drives two Elo ratings per player per season and an achievement layer.

Rating surfaces
2 per player

Per-format Elo + general season Elo. Both Elo, both reset each season.

Match sources
4 ingest paths

Direct pickup, in-tool tournament, Challonge import, Melee import.

Achievement awards
2 paths

Automatic via criteria engine, or manual with player proof and admin grant.

Result ingestion

Every result ends up in the same ledger
Pickup match A submits · B confirms In-tool tournament Swiss + Top 8 Challonge import BYO API key Melee import BYO client credentials Identity resolver matches PlayerExternalAccount · orphans → ImportQueueItem direct direct imports imports Match ledger append-only · source + source_ref + status · bound to a Season and a Format Elo engine format + general + achievements
02 — Architecture

Clean architecture, four layers

Dependencies point inward only. The domain has zero framework imports, use cases depend on port interfaces, and adapters implement those ports. Swapping Challonge for another platform, or SQLAlchemy for another ORM, never touches the domain.

Layer diagram

dependency direction points downward, inward
DRIVERS / FRAMEWORKS React SPA (Node) Kickstarter aesthetic · leaderboards, decklists, admin console HTTP / REST FastAPI · emits OpenAPI from code Future: Discord bot standings posts, game notifications HTTP / imports ADAPTERS / INFRASTRUCTURE PostgresRepos SQLAlchemy · every repository port ChallongeSource TournamentSource port MeleeSource TournamentSource port ScryfallProvider CardProvider port DiscordIdentity IdentityProvider port EncryptedSecretStore BYO keys for Challonge / Melee EventBus adapter match commit fan-out implements ports APPLICATION — USE CASES SubmitPickupMatch · ConfirmPickupMatch · DisputeMatch ReportEliminations · ImportTournament · MapOrphanedImport ApplyMatchResult · RecomputeRatings · GetLeaderboard DefineFormat · DefineSeason · DefineAchievement RequestManualAchievement · ReviewAchievementGrant LinkExternalAccount · ConfigureIntegration CreateTournament · GenerateRoundPairings · SubmitDecklist · UpdateDecklist depends on domain + ports only DOMAIN — PURE, ZERO FRAMEWORK IMPORTS Player · Match · MatchParticipant · MatchElimination · Tournament Season · Format · Rating · RatingChange · Decklist · Achievement EloEngine (placement | kill strategy) · SwissPairing · AchievementCriteria · IdentityMatcher

Ports

interfaces the domain and use cases depend on

Repositories

  • MatchRepository
  • PlayerRepository
  • RatingRepository
  • TournamentRepository
  • AchievementRepository
  • DecklistRepository
  • SeasonRepository
  • FormatRepository
  • ImportQueueRepository

External

  • TournamentSource
  • CardProvider
  • IdentityProvider
  • SecretStore

Infra

  • Clock
  • IdGenerator
  • EventBus

Why it matters here

where the dependency rule pays off
  • TournamentSource
    Challonge, Melee and internal tournaments are interchangeable. Melee's API is gated to approved organizations, so MeleeSource can ship later without touching a single use case. BYO credentials are read through SecretStore, never hard-coded.
  • EloEngine
    Pure domain service. Placement-based and kill-based multiplayer scoring are strategies selected by the season's multiplayer_scoring config. Unit-testable with no database.
  • IdentityMatcher
    Username normalization and matching for imported participants lives here, testable in isolation from the provider clients.
  • AchievementCriteria
    Fixed vocabulary of criteria types evaluated against match fixtures in tests. Manual awards bypass it entirely and go to the admin review queue.
03 — Data Model

Entity relationship diagram

Fifteen tables. The spine is Match → RatingChange: every Elo movement is traceable to the result that caused it. Every rating row is scoped to a Season and resets when a new season opens.

ERD

PK = primary key · FK = foreign key · 1—* = one-to-many
Player id uuid PK display_name avatar_url discord_id uq email is_admin PlayerExternalAccount id uuid PK player_id FK provider discord|melee|challonge external_user_id external_username ← join key Format id uuid PK name · slug is_active Season id uuid PK name · slug status draft|active|closed starts_at · ends_at multiplayer_scoring placement | kill (immutable) creates baseline Rating rows Rating id uuid PK player_id FK · season_id FK format_id FK nullable NULL = general season Elo value · updated_at RatingChange id uuid PK match_id FK · player_id FK rating_id FK · scope old_value · new_value delta (audit trail) Match id uuid PK season_id FK · format_id FK tournament_id FK nullable source direct|internal|challonge|melee source_ref · kind duel|pod status pending|confirmed|applied|disputed played_at · created_at append-only ledger MatchParticipant id uuid PK match_id FK · player_id FK placement (pods) is_winner (duels) games_won · games_lost MatchElimination id uuid PK match_id FK killer_player_id FK victim_player_id FK Tournament id uuid PK name · format_id FK · season_id FK source internal|challonge|melee source_ref · external_url structure swiss|swiss_top8|... status draft|...|completed starts_at · ends_at Decklist id uuid PK tournament_id FK · player_id FK format_id FK · name status draft|submitted|locked submitted_at DecklistEntry id uuid PK decklist_id FK card_ref (scryfall_id) quantity · section Achievement id uuid PK name · description reward_text (free text) award_method automatic|manual criteria json (auto only) is_active PlayerAchievement id uuid PK player_id FK · achievement_id FK status pending|granted|denied proof_text · proof_url awarded_at · awarded_by FK auto awards are created granted IntegrationConfig id uuid PK provider challonge|melee encrypted_credentials enabled · configured_by FK ImportQueueItem id uuid PK tournament_id FK external_username resolved_player_id · status

Relationships

1—* = one-to-many
ParentCardinalityChild
Player1—*PlayerExternalAccount
Player1—*MatchParticipant
Player1—*Rating
Player1—*PlayerAchievement
Player1—*Decklist
Season1—*Match · Tournament · Rating
Format1—*Match · Tournament · Decklist
Tournament1—*Match · Decklist · ImportQueueItem
Match1—*MatchParticipant · MatchElimination
Match1—*RatingChange
Decklist1—*DecklistEntry
Achievement1—*PlayerAchievement

Model invariants

the rules the schema encodes
  • Ratings are season-scoped
    Every Rating row carries a season_id. Both the per-format and the general Elo reset to the baseline (1500) when a new season opens. There is no cross-season skill carry-over.
  • General Elo is format_id = NULL
    One row per (player, season) with format_id null is the general season rating, fed by every match. A row with format_id set is that format's rating within that season.
  • Every Elo move is auditable
    RatingChange is append-only and points at the Match that caused it. POST /admin/ratings/recompute replays the ledger and rebuilds both tables.
  • multiplayer_scoring is immutable after activation
    Placement pods write MatchParticipant.placement. Kill pods write MatchElimination rows, one per pairwise Elo exchange. The engine reads the season config to decide which to use.
  • External username is the identity join key
    Imported participants resolve through PlayerExternalAccount.external_username. Unresolved ones park in ImportQueueItem and their matches hold at pending, blocked from rating application until mapped.
04 — OpenAPI

API surface

Base path /api/v1, JSON, bearer JWT issued by the Discord OAuth exchange. The full machine-readable contract lives in openapi.yaml (OpenAPI 3.1) and is generated directly from the FastAPI route definitions, so code and contract cannot drift.

Operations
41

Across 11 tag groups.

Auth
Bearer JWT

Discord OAuth code exchange, refresh token flow.

Contract
OpenAPI 3.1

Emitted from FastAPI; consumed by the SPA and any future Discord bot.

Auth

Discord OAuth exchange, token refresh, current player.

POST /auth/discord Exchange a Discord OAuth code for an API token

Public. Accepts the authorization code from the Discord OAuth redirect. Creates the Player on first login and records the Discord PlayerExternalAccount link automatically.

Request body required

FieldTypeDescription
code *stringAuthorization code from Discord.
redirect_uri *stringMust match the redirect URI registered with Discord.
code_verifierstringPKCE verifier, if PKCE was used.

Responses

200Token pair and the authenticated player.
FieldTypeDescription
access_token *string
refresh_token *string
expires_in *integerAccess token lifetime in seconds.
player *Player
400Validation failed.
FieldTypeDescription
code *string
message *string
detailsarray of object
401Missing or invalid token.
FieldTypeDescription
code *string
message *string
detailsarray of object
POST /auth/refresh Refresh an access token

Refresh an access token

Request body required

FieldTypeDescription
refresh_token *string

Responses

200New token pair.
FieldTypeDescription
access_token *string
refresh_token *string
expires_in *integer
401Missing or invalid token.
FieldTypeDescription
code *string
message *string
detailsarray of object
GET /auth/me Get the authenticated player

Get the authenticated player

Responses

200The current player, with admin flag.
FieldTypeDescription
id *string
display_name *string
avatar_urlstring
emailstringOnly returned for the authenticated player.
is_admin *boolean
created_at *string
401Missing or invalid token.
FieldTypeDescription
code *string
message *string
detailsarray of object

Players

Public player profiles and self-service account settings.

GET /players List or search players

List or search players

Parameters

NameInTypeDescription
qquerystringCase-insensitive substring match on display name.
season_idquerystringRestrict to players with a rating in this season.
limitqueryinteger
offsetqueryinteger

Responses

200Page of players.
FieldTypeDescription
dataarray of Player
metaPageMeta
GET /players/{id} Get a player profile

Returns the profile with ratings, recent matches, and unlocked achievements.

Parameters

NameInTypeDescription
id *pathstringPlayer id.

Responses

200Full player profile.
FieldTypeDescription
player *Player
ratings *array of Rating
achievements *array of PlayerAchievement
recent_matchesarray of Match
linked_accountsarray of PlayerExternalAccount
404Resource not found.
FieldTypeDescription
code *string
message *string
detailsarray of object
PATCH /players/me Update own profile

Update own profile

Request body required

FieldTypeDescription
display_namestring
avatar_urlstring

Responses

200Updated player.
FieldTypeDescription
id *string
display_name *string
avatar_urlstring
emailstringOnly returned for the authenticated player.
is_admin *boolean
created_at *string
400Validation failed.
FieldTypeDescription
code *string
message *string
detailsarray of object
401Missing or invalid token.
FieldTypeDescription
code *string
message *string
detailsarray of object

Accounts

Linking external identities (Discord, Melee, Challonge).

GET /players/me/accounts List linked external accounts

Returns every PlayerExternalAccount for the caller. A provider may be linked without an API-verified id; in that case external_user_id is null and external_username is a manually declared handle used for identity resolution on import.

Responses

200Linked accounts.
FieldTypeDescription
dataarray of PlayerExternalAccount
401Missing or invalid token.
FieldTypeDescription
code *string
message *string
detailsarray of object
POST /players/me/accounts Link an external account

Link a Discord, Melee, or Challonge identity. Discord is normally linked as a side effect of login and is idempotent. Melee has no public profile API, so a username-only link is the expected shape. Challonge accepts either an OAuth-verified user id or a username-only link.

Request body required

FieldTypeDescription
provider *enum: "discord", "melee", "challonge"
external_user_idstringPlatform-assigned id, when known or verified via OAuth.
external_usernamestringHuman handle. Used for identity resolution on import.

Responses

201Account link created.
FieldTypeDescription
id *string
provider *enum: "discord", "melee", "challonge"
external_user_idstringPlatform-assigned id, when known or OAuth-verified.
external_usernamestringHuman handle. Used as the identity join key on import when no id is available.
verified *booleanTrue when the link was established through the provider's OAuth flow.
created_atstring
400Validation failed.
FieldTypeDescription
code *string
message *string
detailsarray of object
409Resource already exists or state conflict.
FieldTypeDescription
code *string
message *string
detailsarray of object
DELETE /players/me/accounts/{id} Unlink an external account

Unlink an external account

Parameters

NameInTypeDescription
id *pathstring

Responses

204Unlinked. Future imports will no longer resolve to this player via that handle.
404Resource not found.
FieldTypeDescription
code *string
message *string
detailsarray of object

Formats

Admin-defined play formats.

GET /formats List formats

List formats

Parameters

NameInTypeDescription
active_onlyqueryboolean

Responses

200Formats.
FieldTypeDescription
dataarray of Format
POST /formats Create a format

Admin only. admin

Request body required

FieldTypeDescription
name *string
slugstringDerived from name when omitted.
is_activeboolean

Responses

201Created.
FieldTypeDescription
id *string
name *string
slug *string
is_active *boolean
400Validation failed.
FieldTypeDescription
code *string
message *string
detailsarray of object
403Caller lacks admin privileges.
FieldTypeDescription
code *string
message *string
detailsarray of object
409Resource already exists or state conflict.
FieldTypeDescription
code *string
message *string
detailsarray of object
PATCH /formats/{id} Update a format

Admin only. admin

Parameters

NameInTypeDescription
id *pathstringFormat id.

Request body required

FieldTypeDescription
namestring
is_activeboolean

Responses

200Updated.
FieldTypeDescription
id *string
name *string
slug *string
is_active *boolean
403Caller lacks admin privileges.
FieldTypeDescription
code *string
message *string
detailsarray of object
404Resource not found.
FieldTypeDescription
code *string
message *string
detailsarray of object

Seasons

Admin-defined competitive seasons and multiplayer scoring rules.

GET /seasons List seasons

List seasons

Parameters

NameInTypeDescription
statusqueryenum: "draft", "active", "closed"

Responses

200Seasons, newest first.
FieldTypeDescription
dataarray of Season
POST /seasons Create a season

Admin only. multiplayer_scoring is required and immutable once the season moves to active; pick it deliberately. admin

Request body required

FieldTypeDescription
name *string
slugstring
starts_at *string
ends_atstring
multiplayer_scoring *enum: "placement", "kill"How Commander / multiplayer pods are scored for Elo this season.

Responses

201Created.
FieldTypeDescription
id *string
name *string
slug *string
status *enum: "draft", "active", "closed"
multiplayer_scoring *enum: "placement", "kill"How multiplayer pods are scored for Elo this season. Immutable once the season is active.
starts_at *string
ends_atstring
400Validation failed.
FieldTypeDescription
code *string
message *string
detailsarray of object
403Caller lacks admin privileges.
FieldTypeDescription
code *string
message *string
detailsarray of object
PATCH /seasons/{id} Update a season

Admin only. multiplayer_scoring may only be changed while the season is still draft. Changing it after activation returns 409. admin

Parameters

NameInTypeDescription
id *pathstring

Request body required

FieldTypeDescription
namestring
ends_atstring
statusenum: "draft", "active", "closed"
multiplayer_scoringenum: "placement", "kill"

Responses

200Updated.
FieldTypeDescription
id *string
name *string
slug *string
status *enum: "draft", "active", "closed"
multiplayer_scoring *enum: "placement", "kill"How multiplayer pods are scored for Elo this season. Immutable once the season is active.
starts_at *string
ends_atstring
403Caller lacks admin privileges.
FieldTypeDescription
code *string
message *string
detailsarray of object
404Resource not found.
FieldTypeDescription
code *string
message *string
detailsarray of object
409Attempted to change multiplayer_scoring on a non-draft season.
FieldTypeDescription
code *string
message *string
detailsarray of object

Matches

Pickup submissions, confirmations, disputes, and pod elimination reports.

POST /matches Submit a pickup match

Player A submits a result. The match is created with status pending and does not touch any rating until the opponent confirms.

Request body required

FieldTypeDescription
format_id *string
season_id *string
kind *enum: "duel", "pod"duel is 1v1. pod is multiplayer; supply placement on participants when the season is placement-scored, or submit eliminations separately via POST /matches/{id}/eliminations when it is kill-scored.
played_atstringDefaults to now.
participants *array of object

Responses

201Match created in pending state.
FieldTypeDescription
id *string
season_id *string
format_id *string
tournament_idstringNull for pickup matches.
source *enum: "direct", "internal", "challonge", "melee"
source_refstringMatch id on the source platform, when imported.
kind *enum: "duel", "pod"
status *enum: "pending", "confirmed", "applied", "disputed"pending — submitted, awaiting opponent confirmation or import mapping. confirmed — accepted but not yet applied. applied — ratings updated. disputed — under review.
played_atstring
created_atstring
participants *array of MatchParticipant
400Validation failed.
FieldTypeDescription
code *string
message *string
detailsarray of object
404Unknown player, format, or season.
FieldTypeDescription
code *string
message *string
detailsarray of object
GET /matches List matches

List matches

Parameters

NameInTypeDescription
season_idquerystring
format_idquerystring
tournament_idquerystring
player_idquerystring
statusqueryenum: "pending", "confirmed", "applied", "disputed"
sourcequeryenum: "direct", "internal", "challonge", "melee"
limitqueryinteger
offsetqueryinteger

Responses

200Page of matches.
FieldTypeDescription
dataarray of Match
metaPageMeta
GET /matches/{id} Get a match

Get a match

Parameters

NameInTypeDescription
id *pathstringMatch id.

Responses

200Match with participants and eliminations.
FieldTypeDescription
id *string
season_id *string
format_id *string
tournament_idstringNull for pickup matches.
source *enum: "direct", "internal", "challonge", "melee"
source_refstringMatch id on the source platform, when imported.
kind *enum: "duel", "pod"
status *enum: "pending", "confirmed", "applied", "disputed"pending — submitted, awaiting opponent confirmation or import mapping. confirmed — accepted but not yet applied. applied — ratings updated. disputed — under review.
played_atstring
created_atstring
participants *array of MatchParticipant
eliminationsarray of MatchElimination
rating_changesarray of RatingChange
404Resource not found.
FieldTypeDescription
code *string
message *string
detailsarray of object
POST /matches/{id}/confirm Confirm a pickup match

Player B accepts the reported result. On confirmation the match moves to applied, ratings update, and the response returns the resulting RatingChange rows so the UI can show Elo deltas immediately. Only a participant other than the submitter may confirm. A participant may also confirm their own imported match to clear the pending state.

Parameters

NameInTypeDescription
id *pathstringMatch id.

Responses

200Match applied, with rating deltas.
FieldTypeDescription
match *Match
rating_changes *array of RatingChange
403Caller is not a participant, or is the original submitter.
FieldTypeDescription
code *string
message *string
detailsarray of object
404Resource not found.
FieldTypeDescription
code *string
message *string
detailsarray of object
409Match is not in pending state.
FieldTypeDescription
code *string
message *string
detailsarray of object
POST /matches/{id}/dispute Dispute a match

Any participant may move a pending match to disputed. Admin review is required to resolve.

Parameters

NameInTypeDescription
id *pathstringMatch id.

Request body

FieldTypeDescription
reasonstring

Responses

200Match moved to disputed.
FieldTypeDescription
id *string
season_id *string
format_id *string
tournament_idstringNull for pickup matches.
source *enum: "direct", "internal", "challonge", "melee"
source_refstringMatch id on the source platform, when imported.
kind *enum: "duel", "pod"
status *enum: "pending", "confirmed", "applied", "disputed"pending — submitted, awaiting opponent confirmation or import mapping. confirmed — accepted but not yet applied. applied — ratings updated. disputed — under review.
played_atstring
created_atstring
participants *array of MatchParticipant
403Caller lacks admin privileges.
FieldTypeDescription
code *string
message *string
detailsarray of object
409Match is not in pending state.
FieldTypeDescription
code *string
message *string
detailsarray of object
POST /matches/{id}/eliminations Report eliminations for a pod match

Player-reported kill data for kill-scored seasons. Each entry records who eliminated whom and is stored as one pairwise comparison. Idempotent on (match_id, killer, victim).

Parameters

NameInTypeDescription
id *pathstringMatch id.

Request body required

FieldTypeDescription
eliminations *array of object

Responses

201Eliminations recorded.
FieldTypeDescription
dataarray of MatchElimination
400Season is placement-scored, or killer/victim is not a participant.
FieldTypeDescription
code *string
message *string
detailsarray of object
404Resource not found.
FieldTypeDescription
code *string
message *string
detailsarray of object

Tournaments

In-tool tournaments (Swiss / Top 8) and imports from Challonge / Melee.

GET /tournaments List tournaments

List tournaments

Parameters

NameInTypeDescription
season_idquerystring
format_idquerystring
sourcequeryenum: "internal", "challonge", "melee"
statusqueryenum: "draft", "registration", "active", "completed", "cancelled"
limitqueryinteger
offsetqueryinteger

Responses

200Page of tournaments.
FieldTypeDescription
dataarray of Tournament
metaPageMeta
POST /tournaments Create an in-tool tournament

Admin only. Creates the tournament and its participant roster. admin

Request body required

FieldTypeDescription
name *string
format_id *string
season_id *string
structure *enum: "swiss", "swiss_top8", "single_elim", "double_elim", "round_robin"
starts_at *string
ends_atstring
participant_idsarray of stringOptional pre-registration roster.

Responses

201Created.
FieldTypeDescription
id *string
name *string
format_id *string
season_id *string
source *enum: "internal", "challonge", "melee"
source_refstringTournament id on the source platform, when imported.
structure *enum: "swiss", "swiss_top8", "single_elim", "double_elim", "round_robin"
status *enum: "draft", "registration", "active", "completed", "cancelled"
starts_atstring
ends_atstring
external_urlstringLink back to the source platform, when imported.
decklists_availablebooleanTrue when decklists can be attached. Melee imports and internal tournaments are true; Challonge imports are false.
400Validation failed.
FieldTypeDescription
code *string
message *string
detailsarray of object
403Caller lacks admin privileges.
FieldTypeDescription
code *string
message *string
detailsarray of object
POST /tournaments/import Import a tournament from Challonge or Melee

Admin only. Pulls the tournament, its participants, and its matches from the configured provider using the deployment's BYO credentials. Participants are resolved to Player rows via PlayerExternalAccount. Unmatched participants are parked in ImportQueueItem with status: waiting and their matches do not apply to ratings until an admin maps them via POST /admin/import-queue/{id}/map. admin

Request body required

FieldTypeDescription
provider *enum: "challonge", "melee"
source_ref *stringTournament id or slug on the source platform.
season_id *string
format_idstringResolved from the source tournament when omitted.
import_decklistsbooleanPull decklists when the provider exposes them (Melee does; Challonge does not).

Responses

201Import queued/processed.
FieldTypeDescription
tournament *Tournament
import_queue_items *array of ImportQueueItemOne per unresolved participant. Empty when everything matched.
502Upstream provider rejected the request or was unreachable.
FieldTypeDescription
code *string
message *string
detailsarray of object
400Validation failed.
FieldTypeDescription
code *string
message *string
detailsarray of object
403Caller lacks admin privileges.
FieldTypeDescription
code *string
message *string
detailsarray of object
409Provider credentials not configured for this deployment.
FieldTypeDescription
code *string
message *string
detailsarray of object
GET /tournaments/{id} Get a tournament

Includes standings, structure, source metadata, and decklist availability.

Parameters

NameInTypeDescription
id *pathstringTournament id.

Responses

200Tournament detail.
FieldTypeDescription
id *string
name *string
format_id *string
season_id *string
source *enum: "internal", "challonge", "melee"
source_refstringTournament id on the source platform, when imported.
structure *enum: "swiss", "swiss_top8", "single_elim", "double_elim", "round_robin"
status *enum: "draft", "registration", "active", "completed", "cancelled"
starts_atstring
ends_atstring
external_urlstringLink back to the source platform, when imported.
decklists_availablebooleanTrue when decklists can be attached. Melee imports and internal tournaments are true; Challonge imports are false.
participantsarray of TournamentParticipant
roundsarray of Round
standingsarray of Standing
404Resource not found.
FieldTypeDescription
code *string
message *string
detailsarray of object
POST /tournaments/{id}/rounds Generate the next round of pairings

Admin only. For swiss / swiss_top8 structures the domain SwissPairing service computes pairings from current standings and tiebreakers. For elimination structures the next bracket round is generated from prior results. admin

Parameters

NameInTypeDescription
id *pathstringTournament id.

Responses

200Generated round with pairings.
FieldTypeDescription
idstring
numberinteger
statusenum: "pending", "active", "complete"
matchesarray of Match
403Caller lacks admin privileges.
FieldTypeDescription
code *string
message *string
detailsarray of object
409Previous round has unreported matches.
FieldTypeDescription
code *string
message *string
detailsarray of object

Decklists

Per-tournament decklists with Moxfield-style graphic and list views.

GET /tournaments/{id}/decklists List decklists for a tournament

List decklists for a tournament

Parameters

NameInTypeDescription
id *pathstringTournament id.

Responses

200Decklist summaries (without entries).
FieldTypeDescription
dataarray of Decklist
POST /tournaments/{id}/decklists Submit a decklist

A player submits (or replaces) their decklist for a tournament. Entries reference Scryfall card ids; the CardProvider adapter resolves names, sets, and image URLs for the Moxfield-style graphic view.

Parameters

NameInTypeDescription
id *pathstringTournament id.

Request body required

FieldTypeDescription
name *string
format_id *string
entries *array of object

Responses

201Decklist created.
FieldTypeDescription
id *string
tournament_id *string
player_id *string
format_idstring
name *string
status *enum: "draft", "submitted", "locked"
submitted_atstring
entry_countinteger
400Validation failed.
FieldTypeDescription
code *string
message *string
detailsarray of object
403Caller is not a participant of this tournament.
FieldTypeDescription
code *string
message *string
detailsarray of object
409Tournament does not accept decklists (cancelled, or import source without decklist support).
FieldTypeDescription
code *string
message *string
detailsarray of object
GET /decklists/{id} Get a decklist with entries

Returns entries grouped by section plus resolved card metadata and image URLs, sufficient to render both the graphic (card grid) and list views.

Parameters

NameInTypeDescription
id *pathstring

Responses

200Full decklist.
FieldTypeDescription
id *string
tournament_id *string
player_id *string
format_idstring
name *string
status *enum: "draft", "submitted", "locked"
submitted_atstring
entry_countinteger
entriesarray of DecklistEntry
sectionsobjectEntry ids grouped by section, for the graphic view grouping.
404Resource not found.
FieldTypeDescription
code *string
message *string
detailsarray of object
PATCH /decklists/{id} Update a decklist

The owner may edit while the decklist is draft or submitted. Admins may edit in any state.

Parameters

NameInTypeDescription
id *pathstring

Request body required

FieldTypeDescription
namestring
statusenum: "draft", "submitted", "locked"
entriesarray of object

Responses

200Updated.
FieldTypeDescription
id *string
tournament_id *string
player_id *string
format_idstring
name *string
status *enum: "draft", "submitted", "locked"
submitted_atstring
entry_countinteger
403Caller lacks admin privileges.
FieldTypeDescription
code *string
message *string
detailsarray of object
404Resource not found.
FieldTypeDescription
code *string
message *string
detailsarray of object
409Decklist is locked and caller is not an admin.
FieldTypeDescription
code *string
message *string
detailsarray of object

Rankings

Format and general season leaderboards.

GET /rankings Get the leaderboard

Returns Elo standings for a season. Omit format_id for the general season ranking (all matches); set it for a format-specific ranking. Both boards are Elo and both reset when a new season opens. Ranks are dense-ordered by value descending.

Parameters

NameInTypeDescription
season_id *querystring
format_idquerystringOmit for the general season ranking.
limitqueryinteger
offsetqueryinteger

Responses

200Ranked standings.
FieldTypeDescription
season_id *string
scope *enum: "general", "format"general when format_id was omitted, otherwise format.
format_idstringPresent only when scope is format.
format_namestring
data *array of RankingEntry
metaPageMeta
404Unknown season or format.
FieldTypeDescription
code *string
message *string
detailsarray of object

Achievements

Achievement definitions and awards (automatic and manual).

GET /achievements List achievement definitions

List achievement definitions

Parameters

NameInTypeDescription
active_onlyqueryboolean
award_methodqueryenum: "automatic", "manual"

Responses

200Achievement definitions.
FieldTypeDescription
dataarray of Achievement
POST /achievements Create an achievement

Admin only. admin

Request body required

FieldTypeDescription
name *string
descriptionstringHuman-readable criteria. Free text.
reward_textstringFree-text reward description. Not enforced or delivered by the system at this stage.
award_method *enum: "automatic", "manual"
criteriaAchievementCriteria
is_activeboolean

Responses

201Created.
FieldTypeDescription
id *string
name *string
descriptionstringHuman-readable criteria text.
reward_textstringFree-text reward. Not enforced or delivered by the system.
award_method *enum: "automatic", "manual"
criteriaAchievementCriteria
is_active *boolean
400automatic requires criteria; manual must not set it.
FieldTypeDescription
code *string
message *string
detailsarray of object
403Caller lacks admin privileges.
FieldTypeDescription
code *string
message *string
detailsarray of object
PATCH /achievements/{id} Update an achievement

Admin only. admin

Parameters

NameInTypeDescription
id *pathstring

Request body required

FieldTypeDescription
namestring
descriptionstring
reward_textstring
criteriaAchievementCriteria
is_activeboolean

Responses

200Updated.
FieldTypeDescription
id *string
name *string
descriptionstringHuman-readable criteria text.
reward_textstringFree-text reward. Not enforced or delivered by the system.
award_method *enum: "automatic", "manual"
criteriaAchievementCriteria
is_active *boolean
403Caller lacks admin privileges.
FieldTypeDescription
code *string
message *string
detailsarray of object
404Resource not found.
FieldTypeDescription
code *string
message *string
detailsarray of object
POST /achievements/{id}/request Request a manual achievement

Player submits proof for a manual achievement. Creates a PlayerAchievement in pending_review. Admins resolve it via POST /player-achievements/{id}/review.

Parameters

NameInTypeDescription
id *pathstring

Request body required

FieldTypeDescription
proof_text *string
proof_urlstring

Responses

201Request submitted.
FieldTypeDescription
id *string
player_id *string
achievement_id *string
status *enum: "pending_review", "granted", "denied"Automatic awards are created directly as granted.
proof_textstringManual submissions only.
proof_urlstring
awarded_atstringNull while pending_review.
awarded_bystringAdmin who granted a manual award. Null for automatic awards.
400Achievement is not manual, or already requested.
FieldTypeDescription
code *string
message *string
detailsarray of object
404Resource not found.
FieldTypeDescription
code *string
message *string
detailsarray of object
POST /player-achievements/{id}/review Grant or deny a manual achievement

Admin only. admin

Parameters

NameInTypeDescription
id *pathstring

Request body required

FieldTypeDescription
decision *enum: "granted", "denied"
notestring

Responses

200Player achievement updated.
FieldTypeDescription
id *string
player_id *string
achievement_id *string
status *enum: "pending_review", "granted", "denied"Automatic awards are created directly as granted.
proof_textstringManual submissions only.
proof_urlstring
awarded_atstringNull while pending_review.
awarded_bystringAdmin who granted a manual award. Null for automatic awards.
403Caller lacks admin privileges.
FieldTypeDescription
code *string
message *string
detailsarray of object
404Resource not found.
FieldTypeDescription
code *string
message *string
detailsarray of object
409Not in pending_review.
FieldTypeDescription
code *string
message *string
detailsarray of object

Admin

Integration credentials, orphaned import mapping, rating repairs.

GET /admin/integrations List integration configurations

Admin only. Credential material is never included; the response reports only whether credentials are present and when they were last set. admin

Responses

200Integration status.
FieldTypeDescription
dataarray of IntegrationStatus
403Caller lacks admin privileges.
FieldTypeDescription
code *string
message *string
detailsarray of object
PUT /admin/integrations/{provider} Configure BYO credentials for an integration

Admin only. Stores the provider credentials encrypted at rest. Replaces any existing credentials for the provider. Raw credential values are write-only and never echoed back. Access model differs by provider: Challonge issues standard API keys and OAuth clients to any account. Melee restricts API access to organizations and their designated staff and requires approval from contact@melee.gg before issuing a client id and secret. admin

Parameters

NameInTypeDescription
provider *pathenum: "challonge", "melee"

Request body required

FieldTypeDescription
enabled *boolean
credentials *objectProvider-specific key/value material, e.g. Challonge api_key or client_id/client_secret, or Melee client_id/client_secret/org_id. Stored encrypted.

Responses

200Stored. Credentials are not echoed.
FieldTypeDescription
provider *enum: "challonge", "melee"
enabled *boolean
configured *booleanTrue when credential material is present. The material itself is never returned.
configured_atstring
configured_bystring
400Validation failed.
FieldTypeDescription
code *string
message *string
detailsarray of object
403Caller lacks admin privileges.
FieldTypeDescription
code *string
message *string
detailsarray of object
GET /admin/import-queue List orphaned import participants

Admin only. Participants from imported tournaments that no PlayerExternalAccount matched. admin

Parameters

NameInTypeDescription
statusqueryenum: "waiting", "mapped"
tournament_idquerystring

Responses

200Queue items.
FieldTypeDescription
dataarray of ImportQueueItem
403Caller lacks admin privileges.
FieldTypeDescription
code *string
message *string
detailsarray of object
POST /admin/import-queue/{id}/map Map an orphaned participant to a player

Admin only. Resolves the queue item to a Player and applies every parked match for that participant, producing RatingChange rows. Alternatively, a player linking the matching username in Account settings will resolve the item automatically; this endpoint is the manual override. admin

Parameters

NameInTypeDescription
id *pathstring

Request body required

FieldTypeDescription
player_id *string
create_linkbooleanAlso create the PlayerExternalAccount so future imports resolve automatically.

Responses

200Mapped. Includes the rating changes applied from previously parked matches.
FieldTypeDescription
import_queue_item *ImportQueueItem
rating_changes *array of RatingChange
403Caller lacks admin privileges.
FieldTypeDescription
code *string
message *string
detailsarray of object
404Resource not found.
FieldTypeDescription
code *string
message *string
detailsarray of object
409Already mapped.
FieldTypeDescription
code *string
message *string
detailsarray of object
POST /admin/ratings/recompute Recompute ratings for a season

Admin only. Replays the match ledger in played_at / created_at order and rebuilds Rating and RatingChange for the season. Used after corrections to match data or a change in the domain EloEngine policy. admin

Request body required

FieldTypeDescription
season_id *string
format_idstringLimit the rebuild to one format. Omit for the whole season.

Responses

202Recompute accepted.
FieldTypeDescription
job_idstring
statusstring
403Caller lacks admin privileges.
FieldTypeDescription
code *string
message *string
detailsarray of object
i

Source of truth. Click any endpoint to expand its request payload and responses. The authoritative machine-readable contract lives in openapi.yaml (OpenAPI 3.1), emitted from the FastAPI route definitions at build time, so code and contract cannot drift.

05 — Key Decisions

What the model encodes, and why

Design choices that would be expensive to reverse, recorded alongside the shape they produced.

DecisionChoiceRationale
Rating scope Both per-format Elo and general season Elo are Elo, and both are scoped to a Season. Every board is a fresh race when a season opens. Rating.format_id null means general. No cross-season carry-over.
Multiplayer scoring Configured per Season as placement or kill, immutable after activation. One consistent rule per competitive period. Placement pods write placement; kill pods write MatchElimination rows as pairwise exchanges.
Match source Four ingest paths converge on one append-only ledger: direct, internal, challonge, melee. One rating engine, one achievement evaluator, one audit trail regardless of where the result came from.
Identity resolution PlayerExternalAccount.external_username is the join key for imported participants. Melee has no public profile API, so username fallback is the realistic path. Unmatched participants park in ImportQueueItem rather than applying as anonymous results.
Integration credentials Admin brings their own Challonge / Melee keys via PUT /admin/integrations/{provider}, stored encrypted, never returned. Deployment-scoped rather than per-player. Melee restricts API access to approved organizations, so the key belongs to whoever runs the community.
Achievement awards Automatic via a fixed criteria vocabulary, or manual with player proof and an admin grant. Rewards are free text. Fixed vocabulary keeps the evaluator small and testable. Free-text rewards defer the delivery machinery without blocking the feature.
Decklist card data Scryfall ids (card_ref) with a CardProvider port resolving names, sets and images. Scryfall is the community standard and free. The port keeps the display layer independent of any one card source.
Layering Clean architecture: pure domain, use cases over port interfaces, adapters at the edge. Provider integrations (Melee especially) can ship late or be replaced without touching the rating engine or use cases.
Stack Python API (FastAPI) · React SPA (Node) · PostgreSQL. FastAPI emits the OpenAPI contract from code, so the documentation requirement and the API contract are one artifact.
Out of scope Multi-tenancy. Single community per deployment. Avoids threading community_id through every table and query. Revisitable later.
!

External dependency to verify early. Melee's API is gated: access is granted only to organizations and their designated staff, and the org owner must confirm via contact@melee.gg before a client id and secret are issued. MeleeSource sits behind the same TournamentSource port as ChallongeSource, so it can ship later without affecting any use case — but the application should be filed before the integration slice is scheduled.