MTG Copilot Docs
Your data

Pods

What a pod stores, who can read it, how join links and guest players work, and what a logged game records.

A pod is a group of people who play together. Four tables hold one: pods for the group, pod_members for who is in it, pod_invites for the link that lets someone join, and pod_guests for a player with no account.

Every one of the four is readable only by the pod's own members.

Two more tables, pod_visible_deck and pod_visible_profile, hold the narrow slice of your data that pod-mates are allowed to read. They are covered under what sharing a pod shares.

The pod

ColumnWhat it holds
nameThe pod name. Between 1 and 60 characters after trimming.
created_byWho created it. Provenance only, since who administers a pod is the role on pod_members. Clears to null with the account, and the pod stays.
created_atWhen it was created.

Membership

ColumnWhat it holds
pod_id, user_idThe pair is the primary key, so a second join writes nothing rather than a duplicate.
roleowner or member. Owners rename the pod, set roles, and remove people.
joined_atWhen the person joined.

A pod always has at least one owner. Removing the last one raises an error in the database rather than being prevented by a screen, so it holds however the row is deleted.

ColumnWhat it holds
token32 hex characters, generated when the row is created. This is the secret in a join URL.
statusactive or revoked.
expires_atSeven days after the link was created, unless set otherwise.
invited_byWho created the link. Clears to null with the account.
created_atWhen the link was created.

One link admits several people, so an invite has no accepted state. A link works until it is revoked or it expires.

Opening a join link names the pod and whoever created the link, to anyone holding it, signed in or not. That is deliberate: deciding whether to join a pod is deciding what to share, and nobody can decide that against a token that says nothing.

Those two fields are the whole of it. A link reveals no member list, no decks and no pod id, and it grants nothing until somebody signs in and accepts.

Revoking a link takes the pod name back with it. A revoked or expired link stops naming anything.

Guest players

ColumnWhat it holds
display_nameThe name shown for that player. Between 1 and 40 characters, and unique within the pod, ignoring case, among guests that are not archived.
claimed_by_user_idThe account this guest turned out to be, once they and an owner both agreed. Null otherwise, and one-way once set.
claim_requested_byWho says this guest is them, waiting on an owner. Cleared when the claim is confirmed or withdrawn.
archived_atSet when a guest is retired. An archived guest stays readable, and its name becomes free for reuse.
created_byWho added the guest. Clears to null with the account.
created_atWhen the guest was added.

A guest belongs to the pod rather than to a single game, so a pod collects a handful of them and the same name is reused week to week.

Claiming a guest

A guest can turn out to be somebody with an account. Claiming sets claimed_by_user_id on the one guest row, and every seat that guest ever held resolves to that account from then on. No seat is rewritten, which is why forty games can move at once: seats point at the guest row rather than copying a name.

It takes two people. The person says a guest is them, and an owner agrees. Both halves go through database functions, because the two claim columns are not writable by a member at all: row-level security chooses rows and never columns, so the grant that lets a member rename a guest is narrowed to display_name and archived_at.

Claiming is one-way. A second claim on a claimed guest is refused rather than re-pointing somebody's history, an owner cannot confirm their own claim, and one account cannot be two guests in the same pod.

Reading a seat back resolves through game_seat_players, which returns the account each seat belongs to: the member themselves, or whoever claimed that guest, or nobody where a guest is unclaimed.

Logged games

Three tables hold a game that was recorded rather than only tracked: games for the game itself, game_seats for who sat where, and game_events for every change to the table in the order it happened. A game is readable by the members of the pod it was played in.

Naming the table in the tracker writes the game and its seats. Every tap after that is an event, sent in the background, including from a phone with no signal. Game history reads all three back.

The game

ColumnWhat it holds
pod_idThe pod that played it. Required, and the pod cannot be deleted while any of its games exist.
formatOne of commander, standard, modern, legacy, vintage, pioneer, pauper, limited or other.
starting_lifeThe life total every seat began on.
statuslive, finished or abandoned. A game that got packed up unfinished is abandoned and stays out of win rates.
noteAn optional note on the game, up to 2000 characters.
started_at, ended_atWhen it began and when it closed. A live game has no end time, and a closed one always has one.
created_byWho logged it. Clears to null with the account, and the game stays.

The seats

ColumnWhat it holds
seat_indexWhich chair, counting from zero. One row per seat per game, and the key every result is written against.
user_id or guest_idWho sat in that chair by the end. Exactly one of the two, so every seat names somebody the pod already knows.
deck_idThe deck played, where it is a deck in the app. Clears to null if that deck is later deleted.
deck_name_textThe deck's name at the time. Written for every seat, so renaming a deck leaves past games as they were.
final_lifeLife when the game ended.
placementFinishing position, counting from one.
eliminated_atWhen that seat left the game.
is_winnerWhether that seat won. More than one seat can win, and a game can end with none.

The events

ColumnWhat it holds
client_event_idThe id the device gave the event. Unique within a game, which is what makes a re-sent event land once.
client_seqThe emitting device's own counter. It orders that device's events and nothing more.
seat_indexThe seat the event is about, or null for a change to the table, such as passing the turn.
typeWhich change it was, from the list the tracker can emit.
payloadThe change itself, as JSON. Every event is a delta, so a life change records -3 rather than a new total.
occurred_atWhen it happened, taken from the device. created_at is when it reached the server, and the two differ after a game played without signal.
created_byWhich account sent it.

Nobody can edit or delete an event, at any point in a game's life. A finished game accepts no further events.

People trade chairs, and life, damage and counters stay with the chair rather than following the person. So who is in a chair is settled when the game ends, from the seating the events finish on, not from the order the table was set up in. A game logged before that was true reads its results against the wrong people, and its page offers to put it right.

Who can read and write what

A person outside a pod reads zero rows from all four tables. The anon role holds no privilege on any of them, so a request without a session is refused rather than answered with an empty list.

Inside a pod, any member can read everything, add a guest, and create or revoke a join link. Owners additionally rename the pod, change roles, and remove people. Anyone can remove their own membership.

Two writes are refused for everybody, a pod's own owner included:

  • Nothing writes a row into pod_members directly. Being in a pod has to mean that the person agreed to be in it, and an owner who could insert a user id could put anyone in a pod without asking.
  • Nothing writes a row into pods directly. Creating a pod has to create its first owner in the same transaction, or a failure between the two writes leaves a pod that nobody can administer.

Both rules are enforced by the absence of a policy and by the absence of a grant, so neither depends on the other being right.

Games carry two more rules of the same kind:

  • A game can be corrected and cannot be moved. Any member can fix a result: the winner, the placements, the deck a seat was played with, the format, the note. Nobody can change which pod a game belongs to, or which person sat in a seat, because the pod is what decides who may read the game. This one is a column grant rather than a policy, since row-level security chooses rows and never columns.
  • A game is deleted by whoever logged it, or by a pod owner. Its seats and its events go with it. The guests who played keep their rows, because a guest belongs to the pod rather than to one game.

What sharing a pod shares

Two tables answer this, and they are the only way one account reads another's data here. They hold copies of a few fields rather than reaching into your decks, so what follows is everything those tables contain.

Your pod-mates see, for you: your username, and your display name and avatar.

Your pod-mates see, for each of your decks: the deck name, the format, the colors, the commander's name and the commander's card image.

Your pod-mates do not see: the card list, the deck overview, your deck notes, the budget, the meta notes, or your email. Joining a pod grants no access at all to deck_cards, and a pod-mate asking for the cards in a deck they can name still reads zero rows.

The deck name is the label and the commander is secondary, because one person can have two decks with the same commander and a commander alone cannot tell them apart.

Three things worth knowing

Marking a deck private does not hide its name from your pod. Private governs the deck itself, which stays unreadable: the card list, the overview and the notes are closed to a pod-mate whatever the setting. What a pod shares is the name and the commander, so that somebody can put your deck in a seat. A deck you delete leaves the list straight away.

Your display name, avatar and bio are already public, to anyone, signed in or not. That is how profiles work throughout the app and it is not something a pod changes. Your username is the part a pod makes visible.

Leaving a pod ends it. Remove yourself, or get removed, and the decks and names stop resolving on the next read. Nothing is cached on anyone else's account.

How the copies stay current

pod_visible_deck holds a deck's name, format, colors and which card commands it. pod_visible_profile holds a username. Both follow the original the moment you change it, and a deck you delete leaves straight away.

The commander's name and picture are not copied. They come from the card data every visitor can already read, so a pod learns which commander rather than holding a second copy of the card.

On this page