cardforge

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.