Domain Model
Everything in Commander Spellbook is built from a small vocabulary. Learn these six concepts and the rest of the codebase reads easily. The models live in backend/spellbook/models/.
The core concepts
Card
A Magic card, mirrored from Scryfall and keyed by oracle_id (synced by the update_cards task). A Card entity mirrors an oracle card — the game-rules identity shared by every printing — not a specific reprint: there is one Card per oracle card, regardless of how many times it has been printed. Beyond its name it carries the Magic characteristics used for filtering and validation — color identity, mana value, type line, oracle text, keywords — inherited from the abstract Playable base. A card can produce features directly (e.g. a card that by itself is "an extra turn").
Feature
A named effect or result — the abstraction that lets the engine chain things together. Examples: Infinite mana, Untap target permanent, Win the game. Cards and combos produce features; combos need features. A feature's status decides how it is treated and shown:
| Status | Meaning |
|---|---|
| Hidden / Public utility | Intermediate building block used only by the engine (public ones are visible to combo submitters) |
| Helper | A reusable effect meant to be exploited by other combos |
| Contextual | Situational effect |
| Standalone | A meaningful, usually game-impacting result |
Features can be marked uncountable (only ever one copy — this also speeds up generation).
Template
A placeholder for "any card matching a query", e.g. "a creature with power 4 or greater". A template is defined either by a Scryfall-style search query or by an explicit list of concrete replacements (cards known to satisfy it) — not both. Either way the replacements are ordinary Cards. Templates let a combo be written generically; the engine expands them into real cards.
Combo
The editor-authored interaction — the input to the engine. A combo is a recipe that declares:
- uses — cards it needs (
CardInCombo) - requires — templates it needs (
TemplateInCombo) - needs — features it consumes as prerequisites (
FeatureNeededInCombo) - produces — features it results in (
FeatureProducedInCombo) - removes — features it invalidates (
FeatureRemovedInCombo)
plus editorial text (mana needed, prerequisites, step-by-step description, notes). A combo's status controls its role in generation — most importantly GENERATOR (a combo that variants are generated from) versus UTILITY (a building block that only exists to be chained into others).
Variant
A concrete, fully-resolved card combination produced by the engine — the primary object the API serves. Where a combo may say "a mana dork + Isochron Scepter", a variant names the exact cards. Each variant records the generator combos it is of, the combos it includes, the cards it uses, the templates it requires, and the features it produces, and adds derived data: color identity, mana cost, popularity, a power bracket estimate, and a review status (only OK and EXAMPLE variants are public). Variant output is pre-serialized/denormalized on save so reads are fast.
Suggestions
Community-submitted content that waits for editor review before it becomes canonical:
- VariantSuggestion — a combo submitted by a user. Editors review it and, if accepted, turn it into a real Combo.
- VariantUpdateSuggestion — proposed edits to existing variants.
- VariantAlias — a redirect from an old or alternative id to a canonical variant, so links never break.
How they relate
The feature is the pivot: a card or combo produces a feature, and another combo needs it. Chaining "produces → needs" across the graph is exactly what the variant generation engine walks.
Shared building blocks
A few abstract models and join tables recur throughout:
Recipe— the abstractuses/requires/producesstructure and automatic name generation, shared byCombo,Variant, andVariantSuggestion.Playable— Magic characteristics (color identity, mana value, type line, …) shared byCardandVariant.Ingredient/OrderedIngredient— the through-model base that carries per-item data such asquantityand starting zone locations (hand, battlefield, graveyard, …).
The [[name]] and {{name}} reference syntaxes
Text fields (descriptions, prerequisites) can reference a feature by name with [[name]]. Two modifiers exist: [[name|alias]] gives it a reusable alias, and [[name$selector]] selects one of several copies. This lets editorial prose refer to generated pieces without hardcoding card names.
The selector is either a number or an attribute name. A number is the position of the needed feature row among the ones the combo owning the text needs for that feature, so [[name$2]] always means "whatever satisfies the second row", in every variant of that combo — the rows are drag-sortable in the admin. An attribute name, as in [[name$Landfall]], picks the copy produced with that attribute regardless of any ordering. Copies the combo does not ask for stay reachable after the ones it does. Renaming a feature or a feature attribute in the admin rewrites the references to it across every text field, so neither kind of name may contain the $ and | characters the syntax reserves.
{{name}} references the same features, but replaces itself with the text box it is written in — the description with descriptions, the notes with notes — taken from every combo or card feature producing that feature. Those texts are then not appended to the variant's own, because they have already been written where they were mentioned; a field holding no {{name}} at all is assembled by appending, as it always was. Two identical {{name}} write the text twice, an inclusion inside an included text expands in turn, and a text including itself stops there. {{name$selector}} takes the same numbers and attribute names as [[name$selector]] and points at the same thing, filtering which producers are written — the ones it leaves out are still appended. An inclusion nobody produces, like an unresolvable replacement, stays in the text verbatim. It is available in easy_prerequisites, notable_prerequisites, description, notes and comment; mana_needed only takes mana symbols, and the starting card states are merged per card rather than per producer.
Where to go next
The Variant Generation page explains how the engine turns these combos into variants.