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.

Interactive API reference

rendered by Scalar from openapi.yaml
i

Rendered by Scalar. The interactive reference above loads openapi.yaml (OpenAPI 3.1) directly. The same spec is 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.