Card Forge
Developers
Card Forge has no backend — every card, set, and trade offer is a plain JSON document that either lives in your browser (IndexedDB) or travels compressed inside a URL. This page documents those shapes so you can build against them: parse a share link, render a card yourself, or plan an integration ahead of the V2 API.
Card & Template
A Card is pure content — name, stats, art references, chosen rarity. A Template is pure presentation — a list of positioned, styled layers. The renderer is blind: it takes (card, template) and draws pixels with zero knowledge of what any value means. Every visual difference between two cards is data in one of these two documents, never a code branch.
// Card (simplified) — src/lib/schema/card.ts
{
"v": 2,
"id": "c_8f2a1e...",
"kind": "card",
"templateId": "classic",
"design": {
"name": "Ember Warden",
"subtitle": "Guardian of the Coals",
"cardType": "Creature",
"rarity": "rare",
"stats": [{ "label": "Power", "value": 7 }],
"art": [{ "image": { "type": "url", "ref": "https://..." } }],
"ability": "Whenever this card is revealed, it glows.",
"flavor": "The last flame remembers every hand that fed it."
},
"scene": { "enabled": true, "preset": "ember-forge" }
}// Template (simplified) — src/lib/schema/template.ts
{
"id": "classic",
"name": "Classic",
"size": { "width": 750, "height": 1050 },
"rarities": [
{ "id": "rare", "label": "Rare", "color": "#5b8fd6", "holo": "prism" }
],
"layers": [
{
"id": "art-main",
"type": "art",
"anchor": "top-center",
"z": 1,
"size": { "width": "100%", "height": "55%" },
"style": {}
},
{
"id": "name",
"type": "text",
"anchor": "bottom-center",
"z": 10,
"style": { "role": "name", "font": "Cinzel" }
}
]
}CardDesign
Once you save a card to My Cards, it becomes a CardDesign— the card plus the exact template it was built against, under a stable id. A design is a blueprint; it carries no ownership information. That’s what an instance is for.
// CardDesign — src/lib/schema/design.ts
{
"v": 1,
"id": "design_a1b2...",
"name": "Ember Warden",
"updatedAt": "2026-07-01T12:00:00.000Z",
"card": { /* Card, see above */ },
"workingTemplate": { /* Template, see above */ }
}Set & Edition
A CardSet groups designs into a numbered, rarity-tiered expansion within a world (the official Card Forge world, or one a user creates). An Edition describes a finish a card in that set can be minted in — normal, foil, signature, or any other finish registered in src/registries/finishes/, with an optional maximum serial number.
// CardSet (simplified) — src/lib/schema/set.ts
{
"v": 1,
"id": "set_9f3c...",
"worldId": "official",
"name": "Cosmos Rising",
"code": "COS",
"cards": [{ "designId": "design_a1b2...", "collectorNumber": 1, "rarityId": "rare" }],
"rarities": [{ "id": "rare", "label": "Rare", "color": "#5b8fd6" }]
}
// Edition — src/lib/schema/edition.ts
{
"v": 1,
"id": "ed_4a21...",
"setId": "set_9f3c...",
"name": "1st Edition Foil",
"finish": "foil",
"maxSerial": 500
}Instance & provenance
A CardInstanceis a minted, owned copy of a design — the “tokenization” unit. It never embeds the design inline, only references it by id. Every mint, pack pull, trade, or import appends an event to an append-only ledger; an instance’s own log is a filtered view over that ledger, not a separate copy of the truth.
// CardInstance — src/lib/schema/instance.ts
{
"v": 1,
"id": "01J8X3ZQ7K9YV2N4M6P8R1S3W5", // ULID — sortable by mint time
"designId": "design_a1b2...",
"setId": "set_9f3c...",
"editionId": "ed_4a21...",
"serial": 217,
"mintedAt": "2026-07-01T12:05:00.000Z",
"log": [{ "at": "2026-07-01T12:05:00.000Z", "kind": "mint" }]
}kind is one of a small closed set: mint, pack-open, trade-in, trade-out, import.
Packs
A PackConfigdescribes a booster: one or more slots, each either a weighted rarity pool or an explicit card list, plus optional guarantees (“at least N of rarity X”) and pity counters (“guaranteed at least once every N packs”). The pull engine is a pure function of a config, the set, and an injected random source — fully deterministic and independently testable.
// PackConfig (simplified) — src/lib/schema/pack.ts
{
"v": 1,
"id": "pack_77c1...",
"setId": "set_9f3c...",
"name": "Booster Pack",
"slots": [
{
"pool": { "kind": "weighted", "weights": { "common": 70, "rare": 25, "mythic": 5 } },
"count": 8
}
],
"guarantees": [{ "rarityId": "rare", "minPerPack": 1 }],
"pity": [{ "rarityId": "mythic", "everyNPacks": 12 }]
}Trade offers
Trading has no server, so there is no atomic escrow — a trade is a share link, exactly like a card link. The offering side builds a TradePayload that embeds full snapshots of the offered instances and the designs they reference (the receiving browser has no local copy of either), compresses it the same way a card link is compressed, and shares the URL. Accepting and completing are two independent, trust-based actions — see /trades for the UI built on top of this.
// TradePayload — src/lib/schema/trade.ts
{
"v": 1,
"offer": {
"v": 1,
"id": "trade_5b9e...",
"createdAt": "2026-07-01T12:10:00.000Z",
"give": ["01J8X3ZQ7K9YV2N4M6P8R1S3W5"],
"want": { "note": "any foil rare" },
"status": "open",
"log": [{ "at": "2026-07-01T12:10:00.000Z", "kind": "created" }]
},
"instances": [ /* CardInstance snapshots referenced by offer.give */ ],
"designs": [ /* CardDesign snapshots those instances need to render */ ]
}
https://cardforge.example/trades/accept/N4IgzgpgTgpg9gLQ...
^^ path segment = lz-string(JSON.stringify(payload))Marketplace listings
A Listingoffers one owned instance at a price in a placeholder currency (“Forge Credits”) — creating, browsing, and cancelling a listing is real; there is no buy flow yet, since V1 has no accounts or payments. status: "sold" exists in the shape already so a future real marketplace can reuse it unchanged.
// Listing — src/lib/schema/market.ts
{
"v": 1,
"id": "listing_2c7a...",
"instanceId": "01J8X3ZQ7K9YV2N4M6P8R1S3W5",
"price": 40,
"note": "mint condition",
"createdAt": "2026-07-01T12:15:00.000Z",
"status": "active"
}What’s next
Real accounts, cross-device sync, and a payable marketplace are the next real architectural changes — see the roadmap. Every schema here follows the same rule: new fields are always optional or defaulted, so a link generated today keeps decoding forever.