Decks
What a deck row holds, how the card list and its history are stored, who can read a deck, and what deck import and export accept.
A deck is one row in decks plus one row per card in deck_cards. Five more
tables hang off those two: folders for grouping, deck_shares for access,
deck_card_events for history, and deck_analyses with
deck_analysis_feedback for the Analytics tab.
Every one of them is scoped by row-level security. The policy on decks is
auth.uid() = user_id, and the other six apply the same check, either on their
own user_id or through the deck the row belongs to. A new deck is private
(is_private defaults to true).
The deck row
decks carries the name and commander, and then everything you fill in on the
Design tab.
| Column | What it holds |
|---|---|
name | The deck name. |
user_id | You. The column every policy on this table checks. |
folder_id | The folder it sits in, or null for the top level. |
commander_card_id | The commander. |
description | Set when a deck is cloned or built from a precon, and blank otherwise. |
colors, accent_color | Deck colors, and the single color used for the deck's accent. |
is_private | true by default. |
power_level | 1 to 10, enforced by a check constraint. |
play_style, themes | Free text and a list of theme strings. |
budget_usd, win_conditions | Your stated budget and win conditions. |
meta_notes | What you expect to play against. |
must_include_card_ids | Cards the AI is told to keep. |
design_locks | Which design fields are pinned against AI edits. |
turn_by_turn_plan | The game plan, keyed by turn. |
overview, overview_doc | The deck overview. The doc is the editor's JSON, the text its markdown form. |
deck_notes | Up to four named notes, each with the same doc and markdown pair. |
available_tags | The tag vocabulary you can apply to cards in this deck. |
settings | Per-deck UI settings, merged on write. |
created_at, updated_at | Timestamps. |
Two more columns hold vectors. overview_embedding and game_plan_embedding
are computed from the overview and the turn plan when a recommendation run needs
them, and embedded_at records when that happened, so the next run can reuse
them.
deck_idea, wincon and theme are older columns that nothing in the app
writes today.
The card list
deck_cards holds one row per card per board.
| Column | What it holds |
|---|---|
card_id | The card. Printings are a separate concept, below. |
board | main, sideboard, maybeboard or commander. |
quantity | Copies on that board. Defaults to 1. |
printing_id | The printing you pinned, if you pinned one. |
is_core | Marks a card as core to the deck. |
tags | Your tags for this card, drawn from the deck's available_tags. |
notes | Your note on this card in this deck. |
added_at | When the row was created. |
The commander lives in decks.commander_card_id. commander is still a legal
value for board because older decks stored it that way, and cloning a deck
drops those rows rather than copying them.
Pinning a printing changes nothing about the card itself. It decides which set code and collector number a text or CSV export writes out.
Folders
folders is a name, a user_id, and a parent_folder_id, so folders nest.
Deleting one keeps its contents: decks inside it have folder_id cleared, child
folders have parent_folder_id cleared, and both end up at the top level.
History
Every change to deck_cards writes a row to deck_card_events. The write
happens in a Postgres trigger on the table itself, so manual edits, an AI
suggestion you accept, an import and a clone all record the same way.
| Column | What it holds |
|---|---|
event_type | added, removed or moved. |
board | The board landed on, or left for a removal. |
from_board | The board left, on a move. |
quantity | Copies affected, always positive. |
card_name | The card's name as of the event. |
tags, notes | The card's tags and note at the time. |
source | manual or ai-recommendation. |
created_at | When it happened. |
A quantity change records as an added or removed event for the difference,
so raising a count from 1 to 3 records an added event for 2. Changing only a
card's tags or note records nothing.
The card name is copied onto the event so an entry still reads correctly later. The history panel shows the 50 most recent events for a deck.
Analyses
deck_analyses stores one row per analysis runner per run. Nine runners exist:
staple identification, top performers, recommended cuts, cohesion, power,
explosivity, design alignment, ramp, and an overall summary. runner_id names
the one that produced the row and result holds its parsed output.
deck_hash is the first 16 characters of a SHA-256 over the deck's card ids and
quantities, sorted. Comparing it against the deck as it stands now is how a
stored analysis is known to be out of date. The Analytics tab reads the 40 most
recent rows for a deck.
deck_analysis_feedback records agreeing or disagreeing with one item inside an
analysis. item_type and item_value identify the item, feedback_type is
agree or disagree, note is your optional comment, and analysis_id points
back at the run.
Who can read a deck
| Reader | Gets |
|---|---|
| You | Full access to your decks and everything hanging off them. |
| Anyone | A deck, its cards, and its analyses once you set is_private to false. |
| A named collaborator | Read access through deck_shares. |
deck_shares pairs a deck with a user at a permission of view_only, edit
or owner. The policies on decks and deck_cards respect it, so a share row
would grant read access. No screen in the app creates one today, which leaves
public and private as the only two states you can actually reach.
Making a deck public exposes its analyses along with its cards. Deck history, notes and design fields stay owner-only.
Import and export
Deck import takes pasted text and works out what it is from the first non-empty
line. A line containing a comma and one of count, quantity, name,
edition, set code or foil is read as a CSV using the same header-name
rules as collection import, and every row lands on
the mainboard. Anything else is read as a decklist, where section headers put
cards on the sideboard or maybeboard.
Cards resolve by exact name, case-insensitive, preferring a non-token card when a name matches both. Import previews the result first, so you see how many lines matched and a sample of the ones that did not before anything is written. A line that matches nothing is skipped and named in the result.
Export gives you either a text decklist, grouped by section and optionally carrying the set code and collector number of each pinned printing, or a CSV in the collection format.
What the AI is given
Answering a question about a deck means reading it, so the deck's cards and design fields are loaded server-side as part of your own request. See how the AI sees your deck for what is loaded when.