# How TCG Data Is Organized in CardsDB: Expansions, Rarity, and the Critical internal_id System

> Discover how tcg data is organized in CardsDB. Learn about expansions, rarity, and the crucial internal_id system for linking application data.

- Repository: [Marcel Panse/tcg-pocket-collection-tracker](https://github.com/marcelpanse/tcg-pocket-collection-tracker)
- Tags: internals
- Published: 2026-03-06

---

**The TCG Pocket Collection Tracker centralizes all card metadata in [`frontend/src/lib/CardsDB.ts`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/frontend/src/lib/CardsDB.ts), using stable numeric `internal_id` values as immutable keys to link cards, expansions, and missions across the entire application.**

The repository `marcelpanse/tcg-pocket-collection-tracker` implements a high-performance data layer that lazily loads JSON assets and constructs in-memory indexes for instant lookups. Understanding this **TCG data organization** is essential for contributors working with card collections, pack openings, or trade systems.

## Data Architecture and Lookup Indexes

The [`CardsDB.ts`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/CardsDB.ts) file serves as the single source of truth for the frontend. It imports the master card list from [`cards.json`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/cards.json) and themed-collection mission files, then builds optimized dictionaries for O(1) access patterns.

### Card Indexes and Lookup Dictionaries

Upon initialization, the module creates three critical lookup structures:

- **`allCardsDict`** – Maps `card_id` strings (e.g., `"A1-001"`) to full `Card` objects for fast public-ID lookups.
- **`allCardsByInternalId`** – Maps numeric `internal_id` values to `Card` objects, maintaining reverse-chronological order using `toReversed()`.
- **`allCardsByInternalIdList`** – Groups cards sharing the same `internal_id`, enabling queries for alternate art variants or duplicate entries.

These dictionaries power the exported helper functions `getCardById`, `getCardByInternalId`, and `getCardsByInternalId`. The implementation at lines 22-25 of [`frontend/src/lib/CardsDB.ts`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/frontend/src/lib/CardsDB.ts) ensures that UI components never scan the full card array.

### Expansion Definitions and Metadata

The `expansions` array (lines 62-165) defines every set with precise metadata:

- **`name`** – Machine-readable slug (e.g., `genetic-apex`)
- **`id`** – Public expansion code (e.g., `A1`, `A2`)
- **`internalId`** – **Numeric stable identifier** (e.g., `1`, `2`, `192`)
- **`packs`** – Array of pack objects containing `name` and UI `color` values
- **`missions`** – Imported mission arrays from JSON files like [`A1-missions.json`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/A1-missions.json)
- **Feature flags** – `tradeable`, `openable`, `promo` booleans
- **`packStructure`** – Object defining shiny pools, baby Pokémon presence, and cards-per-pack counts

Promo expansions occupy reserved high ranges (`192`, `193`) and carry `promo: true` flags, physically separating them from core sets to prevent ID collisions.

### Rarity and Crafting Cost Mappings

The database enumerates rarity tiers through two exported constants:

- **`basicRarities`** – Array of the four standard diamond rarities (◆ to ◆◆◆◆)
- **`craftingCost`** – Dictionary mapping every rarity string (including stars and Crown Rare) to its crafting point cost

This structure decouples display logic from economic rules, allowing the UI to calculate deck costs without hardcoding values.

## The Significance of the internal_id System

The `internal_id` field is not merely an alternative key—it is the backbone of data integrity throughout the application.

### Stability Across Versions

Each expansion definition contains an explicit comment warning: `// IMPORTANT note: these should NEVER EVER change. The internals of the DB depend on it.` This immutability contract ensures that:

- Collection records remain valid after app updates
- URL parameters and shared links resolve correctly
- Backend edge functions can cache numeric IDs without invalidation risks

### Performance and Cross-Entity References

Numeric `internal_id` values provide measurable advantages over string identifiers:

- **Memory efficiency** – Maps with integer keys consume less memory than string-keyed objects
- **Query optimization** – Supabase edge functions and query caches store `internal_id` instead of textual `id` to minimize payload size and eliminate case-sensitivity issues
- **Sorting consistency** – The UI uses `internal_id` to order expansions chronologically regardless of alphabetical naming changes

### Duplicate Card Grouping

The `internal_id` system elegantly handles alternate artwork and promotional reprints. Cards sharing the same `internal_id` (accessed via `getCardsByInternalId`) represent functional duplicates, allowing the collection tracker to display "owned" status across variants while maintaining distinct card records for completion tracking.

## Working with CardsDB: Practical Examples

### Lookup by Public Card ID

Retrieve full card details using the human-readable identifier:

```typescript
import { getCardById } from '@/lib/CardsDB'

const card = getCardById('A1-001')
if (card) {
  console.log(`${card.name} is a ${card.rarity} from expansion ${card.expansion_id}`)
}

```

This utilizes the `allCardsDict` mapping (lines 26-28) for instant retrieval.

### Query Expansion Metadata

Access pack structures and mission data through the expansion helper:

```typescript
import { getExpansionById } from '@/lib/CardsDB'

const expansion = getExpansionById('A2')
console.log(`Expansion ${expansion.name} contains:`)
expansion.packs.forEach(pack => {
  console.log(`- ${pack.name} (${pack.color})`)
})

```

The `getExpansionById` function (lines 34-40) returns the full `Expansion` object including the critical `internalId` property.

### Find Duplicate Card Variants

Identify all cards sharing the same internal identifier, useful for alternate art detection:

```typescript
import { getCardsByInternalId } from '@/lib/CardsDB'

const variants = getCardsByInternalId(42) ?? []
console.log(`Found ${variants.length} card variants for internal_id 42`)

```

This queries the `allCardsByInternalIdList` index (line 24) built during module initialization.

### Calculate Crafting Costs

Determine the resource cost for any rarity tier:

```typescript
import { craftingCost } from '@/lib/CardsDB'

function getCraftingPoints(rarity: string): number {
  return craftingCost[rarity] ?? 0
}

console.log(`Crown Rare costs ${getCraftingPoints('Crown Rare')} points`)

```

## Summary

- **[`frontend/src/lib/CardsDB.ts`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/frontend/src/lib/CardsDB.ts)** serves as the centralized data layer, lazily loading [`cards.json`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/cards.json) and mission assets to build optimized lookup dictionaries.
- The **`internal_id`** system provides immutable numeric keys for expansions and cards, ensuring stable references across app versions and enabling fast Map-based lookups.
- **Lookup dictionaries** (`allCardsDict`, `allCardsByInternalId`, `allCardsByInternalIdList`) deliver O(1) access patterns for card queries and duplicate grouping.
- **Expansion metadata** includes pack structures, mission arrays, and feature flags (`tradeable`, `promo`), with promo sets occupying reserved high-value `internal_id` ranges (192, 193).
- **Rarity handling** separates display categories (`basicRarities`) from economic rules (`craftingCost`), decoupling UI logic from game mechanics.

## Frequently Asked Questions

### Why does CardsDB use both card_id and internal_id?

The `card_id` (e.g., `"A1-001"`) provides human-readable, public-facing identifiers suitable for URLs and user displays, while the numeric `internal_id` offers machine-optimized keys for internal lookups, sorting, and cross-referencing duplicate variants. This separation allows the public API to remain descriptive while the internal database maintains high-performance integer indexing.

### What happens if an internal_id value changes?

Changing an `internal_id` would break the relationship between card records and their expansion metadata, causing runtime errors in `getCardsByInternalId` and invalidating user collection data. The codebase explicitly marks these values as immutable with comments stating they should "NEVER EVER change" because backend caches, URL parameters, and the `allCardsByInternalId` dictionary depend on these stable numeric keys.

### How does the database handle promo cards and special expansions?

Promo expansions use high-range `internal_id` values (specifically `192` and `193` in the current implementation) and set the `promo: true` flag in their expansion definitions. This reserved numbering scheme ensures promotional sets never collide with future core expansions while still integrating into the same lookup dictionaries and mission systems as standard sets.

### Where is rarity data defined and how is it used?

Rarity definitions live in the `basicRarities` array and `craftingCost` dictionary exported from [`frontend/src/lib/CardsDB.ts`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/frontend/src/lib/CardsDB.ts) (lines 44-55). The `craftingCost` mapping specifically translates rarity strings (including "Crown Rare" and star rarities) into numeric crafting point values, enabling the deck builder and collection tracker to calculate resource requirements without hardcoding economic constants.