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.
Per-format Elo + general season Elo. Both Elo, both reset each season.
Direct pickup, in-tool tournament, Challonge import, Melee import.
Automatic via criteria engine, or manual with player proof and admin grant.
Result ingestion
Every result ends up in the same ledgerClean 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, inwardPorts
interfaces the domain and use cases depend onRepositories
- 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-
TournamentSourceChallonge, Melee and internal tournaments are interchangeable. Melee's API is gated to approved organizations, so
MeleeSourcecan ship later without touching a single use case. BYO credentials are read throughSecretStore, never hard-coded. -
EloEnginePure domain service. Placement-based and kill-based multiplayer scoring are strategies selected by the season's
multiplayer_scoringconfig. Unit-testable with no database. -
IdentityMatcherUsername normalization and matching for imported participants lives here, testable in isolation from the provider clients.
-
AchievementCriteriaFixed vocabulary of criteria types evaluated against match fixtures in tests. Manual awards bypass it entirely and go to the admin review queue.
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-manyRelationships
1—* = one-to-many| Parent | Cardinality | Child |
|---|---|---|
| Player | 1—* | PlayerExternalAccount |
| Player | 1—* | MatchParticipant |
| Player | 1—* | Rating |
| Player | 1—* | PlayerAchievement |
| Player | 1—* | Decklist |
| Season | 1—* | Match · Tournament · Rating |
| Format | 1—* | Match · Tournament · Decklist |
| Tournament | 1—* | Match · Decklist · ImportQueueItem |
| Match | 1—* | MatchParticipant · MatchElimination |
| Match | 1—* | RatingChange |
| Decklist | 1—* | DecklistEntry |
| Achievement | 1—* | PlayerAchievement |
Model invariants
the rules the schema encodes-
Ratings are season-scopedEvery
Ratingrow carries aseason_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 = NULLOne row per (player, season) with
format_idnull is the general season rating, fed by every match. A row withformat_idset is that format's rating within that season. -
Every Elo move is auditable
RatingChangeis append-only and points at theMatchthat caused it.POST /admin/ratings/recomputereplays the ledger and rebuilds both tables. -
multiplayer_scoring is immutable after activationPlacement pods write
MatchParticipant.placement. Kill pods writeMatchEliminationrows, one per pairwise Elo exchange. The engine reads the season config to decide which to use. -
External username is the identity join keyImported participants resolve through
PlayerExternalAccount.external_username. Unresolved ones park inImportQueueItemand their matches hold atpending, blocked from rating application until mapped.
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.
Across 11 tag groups.
Discord OAuth code exchange, refresh token flow.
Emitted from FastAPI; consumed by the SPA and any future Discord bot.
Interactive API reference
rendered by Scalar from openapi.yaml
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.
What the model encodes, and why
Design choices that would be expensive to reverse, recorded alongside the shape they produced.
| Decision | Choice | Rationale |
|---|---|---|
| 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.