How to Build Progression

Battles award XP and currency. XP unlocks creature upgrades: HP, traits, cards. Currency creates new creatures. This is the plan for building the server-side infrastructure to support it.

2026-04-10 Context: PR #842 + #829 review
01 / The Problem

Missing Progression Infrastructure

According to the GDD, creatures earn XP and currency (Bobium). XP unlocks HP, traits, adding and replacing cards. Bobium unlocks creature creation. These rewards are the core economy and the basis of monetization.

None of this has server-side infrastructure today. Our Cloudflare worker determines the winner of every battle, then discards that data. Battles, rewards, and upgrades are not recorded or tracked.

Without server authority over progression, the economy is unenforceable. Players could set any XP, level, HP, or currency value. We cannot sell progression content, run competitive ladders, enforce exhaustion cooldowns, or trust creature stats.

02 / Principles

Principles

Principle 1

Battles are first-class entities

Every battle gets a row with participants, outcome, timestamp, and rewards granted. This is the provenance root for the entire economy.

Supercell engineering blog; Pokemon GO server postmortem (GDC 2017). "If it affects gameplay and you didn't log it, it didn't happen."

Principle 2

Rewards reference their source

Every XP and currency award has a source_type and source_id. Without this you can't distinguish earned rewards from exploited ones, and you can't tune reward rates per battle type.

Pokemon GO (GDC 2017): when they found GPS spoofers, every XP award traced back to a specific event with a location and timestamp.

Principle 3

Progression is append-only

Each state change is a ledger entry, not an in-place UPDATE. You can reconstruct any creature's state at any point in time. Totals are cached sums. If they drift, recompute from the ledger.

Supercell engineering blog: "Every gem, every gold coin, every XP point has a ledger entry. The balance column is a cache. We recompute quarterly and always catch drift."

Principle 4

Choices are recorded with their full context

When a player levels up and picks from offered options, store what was offered, what was selected, and what the creature state was at the time. This enables balance analytics, dispute resolution, and future respec features.

Schell, Art of Game Design (Lens of the Progression).

03 / The Plan

7 PRs, 4 steps

We build this as a stack of PRs, each adding a layer. The first three can be built at the same time. The rest build on top of them in order.

STEP 1 FOUNDATION STEP 2 CONNECT STEP 3 AUTHORITY STEP 4 LOCKDOWN PR 1 Track battles in Supabase Players, creatures, outcomes per battle PR 2 Add progression tables Schema for upgrades and level-up history PR 3 Build the progression service Abstract API for rewards and leveling PR 4 Create the XP awards ledger Append-only record of every reward PR 5 Claim rewards at battle end Wire progression into the game lifecycle PR 6 Make progression server-authoritative Server validates, calculates, and writes all rewards PR 7 Lock down client database access

PR 1 / Track battles in Supabase

When a battle finishes, the server writes a row to a new battles table: who played, which creatures, who won, how many rounds. The battle ID is also sent to the client so it can reference it later when claiming rewards.

Step 1 / No dependencies / Needed by PR 4 and PR 6
Planned

PR 2 / Add progression tables #829

Database tables for tracking creature upgrades. One table records each level-up choice (what was picked, creature state snapshot). Another stores the generated options that were offered. Also adds XP and max_hp columns to the creatures table.

Step 1 / No dependencies / Needed by PR 6
Open

PR 3 / Build the progression service #842

An abstract service class that defines the API for claiming battle rewards and leveling up. A local implementation handles it on-device for development. The server implementation comes in PR 6. Typed data structures for reward claims, battle rewards, and level-up results.

Step 1 / No dependencies / Needed by PR 5
Rework

PR 4 / Create the XP awards ledger

An append-only table where every XP and currency award is a row, each referencing the battle it came from. The creature's total XP is a cached sum updated in the same transaction. If the cache ever drifts, it can be recomputed from the ledger.

Step 2 / Needs PR 1
Planned

PR 5 / Claim rewards at battle end

When a battle finishes, the progression service is notified through the game lifecycle (not through the result screen UI). Rewards are claimed and persisted before the player sees anything. The result screen only displays what already happened.

Step 2 / Needs PR 3
Planned

PR 6 / Make progression server-authoritative

Replace the local progression service with one that calls the server. The client sends a battle ID and creature ID. The server checks the battle happened, calculates the reward, writes it to the ledger, and returns the result. The client never writes progression data directly.

Step 3 / Needs PR 2 + PR 4 + PR 5
Planned

PR 7 / Lock down client database access

Remove the local progression service from production builds. Restrict XP, level, and HP columns so only the server can write them. Add scripts to verify the ledger matches the cached totals.

Step 4 / Needs PR 6
Planned
04 / Where We Are

Current state and what each PR fixes

Each box on the left is something that exists in the codebase today. The arrow points to the PR that addresses it.

Server knows who won, then discards it No record of any battle in the database XP is a mutable column with no history Client writes XP and level directly to DB Rewards triggered inside a UI animation PR 1 Track battles in Supabase PR 2 Add progression tables PR 4 Create the XP awards ledger PR 6 Make progression server-authoritative PR 5 Claim rewards at battle end
05 / Table Details

Database schemas

battles

New

For every battle we need to know: who played, which creatures they used, what the outcome was, and when it happened. Every reward in the system traces back to a row in this table.

ColumnTypePurpose
idUUID PKGame ID from Cloudflare DO. Reused, not generated.
statusTEXTcompleted / abandoned / errored
outcomeTEXTp1_win / p2_win / draw / forfeit
player1_idUUID FKFirst player
player2_idUUID FKSecond player
player1_creature_idUUID FKCreature used by player 1
player2_creature_idUUID FKCreature used by player 2
winner_idUUID FKNullable for draws
total_roundsINTRounds played
duration_msINTBattle duration
started_atTIMESTAMPTZ
completed_atTIMESTAMPTZ

RLS: SELECT own battles. INSERT/UPDATE: service_role only. Written by the server at GAME_COMPLETE.

xp_awards

New

Every time a creature earns XP or currency, it's a row here. Each row references the battle it came from. The creature's total XP on the creatures table is a cached sum of these rows, updated in the same transaction.

ColumnTypePurpose
idUUID PK
creature_idUUID FKWhich creature earned it
battle_idUUID FKWhich battle it came from
amountINTXP awarded
reasonTEXTbattle_win / battle_loss / battle_draw / bonus
created_atTIMESTAMPTZ

RLS: SELECT own. INSERT: service_role only. Recomputable: if cache drifts, SELECT creature_id, SUM(amount) FROM xp_awards GROUP BY creature_id.

creature_progression

From PR #829

When a creature levels up, the player picks from several upgrade options. This table records which option was picked, along with a snapshot of the creature's deck and stats at that moment, so the history can be replayed or audited.

ColumnTypePurpose
idUUID PK
creature_idUUID FK
levelINTLevel reached
xp_snapshotINTCumulative XP at level-up time
hp_snapshotINTmax_hp after this event
deck_snapshotJSONBFrozen deck state after this event
choice_typeTEXTincrease_hp / upgrade_card / add_card / replace_card
hp_deltaINTHP increase (if increase_hp)
target_card_indexINTDeck slot affected (if card change)
selected_option_indexINTWhich generated card was picked
created_atTIMESTAMPTZ

UNIQUE (creature_id, level). RLS: SELECT own. INSERT: service_role only.

creature_progression_cards

From PR #829

The upgrade options generated for a level-up. Stored separately because they can be pre-generated before the player reaches that level. This is what was offered, per Principle 4.

ColumnTypePurpose
idUUID PK
creature_idUUID FK
levelINTLevel these options are for
target_card_indexINTCard slot being replaced (null for add)
card_optionsJSONBArray of generated card objects
generation_sourceTEXTon_demand / pre_generated
created_atTIMESTAMPTZ

UNIQUE (creature_id, level, COALESCE(target_card_index, -1)). RLS: SELECT own. INSERT: service_role only.