Step 1

Load your collection

Upload a CSV export of the cards you own. Every row is matched against Scryfall — type, color identity, legality, price and EDHREC rank — so the deck that follows is grounded in real data.

Drop your CSV here

Or pick a file from your computer. One row per card is enough; quantities are optional.

Command Forge looks for a name column (Name, Card Name, Karte) and an optional quantity column (Quantity, Count, Anzahl). Everything else is ignored, so exports from Manabox, Moxfield, Archidekt, Deckbox and Delver Lens work as-is. Comma and semicolon delimiters are both handled.
Step 2

Your collection

Every card matched from your CSV. Organise cards into binders, filter by color identity or search by name.

Step 3

Choose your commander

Legendary creatures from your own collection, or the most-played commanders that fit your colors.

Step 4

Decks

Every deck you've built, saved locally. Open one to edit it card by card, or start a new one from a commander.

Scan cards

Point the camera at a card so its name sits inside the dashed band, then capture. The name is read on-device and matched against Scryfall — you confirm every card before it enters your collection.

Camera is off. Start it below, or take a photo with your phone's normal camera and upload it instead.

Match

No card read yet. Capture a card, or type a name below if the camera is unavailable.

Settings

Optional configuration. Everything here is stored locally in your browser.

Signing in stores your collection, binders and decks in your own Supabase project so they follow you between devices. Without an account everything stays in this browser only.
Found under Project Settings → Data API. Looks like https://abcdefgh.supabase.co.
The publishable (anon) key is meant to be visible in the browser. Never put a sb_secret_… key here — it bypasses row level security.
Run this once in the Supabase SQL editor. It creates the table and locks each row to its owner.
create table if not exists public.command_forge (
  user_id    uuid primary key references auth.users(id) on delete cascade,
  payload    jsonb       not null default '{}'::jsonb,
  updated_at timestamptz not null default now()
);

alter table public.command_forge enable row level security;

create policy "read own row"   on public.command_forge
  for select using (auth.uid() = user_id);
create policy "insert own row" on public.command_forge
  for insert with check (auth.uid() = user_id);
create policy "update own row" on public.command_forge
  for update using (auth.uid() = user_id) with check (auth.uid() = user_id);
Which market feed prices and collection value are shown in. Scryfall carries both: TCGplayer for US dollar prices, Cardmarket for euro prices. If a card has no price in your chosen feed, the other one is shown instead and marked with a tilde.
EDHREC has no official public API. Its JSON endpoint may or may not permit direct browser access depending on CORS headers — when it is blocked, Command Forge falls back to Scryfall's EDHREC ranking, which always works but carries no per-commander synergy percentages. For reliable synergy data, deploy the worker below and paste its URL here. Leave empty to try EDHREC directly.
Create a Worker at dash.cloudflare.com, paste this, deploy, then put its URL in the field above.
export default {
  async fetch(request) {
    const url  = new URL(request.url);
    const path = url.pathname.replace(/^\/+/, "");
    if (!path) return new Response("ok");

    if (request.method === "OPTIONS") return new Response(null, { headers: cors() });

    const upstream = await fetch("https://json.edhrec.com/" + path, {
      headers: { Accept: "application/json" },
      cf: { cacheTtl: 86400, cacheEverything: true }
    });
    return new Response(upstream.body, {
      status: upstream.status,
      headers: { ...cors(), "Content-Type": "application/json" }
    });
  }
};
function cors() {
  return {
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Methods": "GET,OPTIONS",
    "Cache-Control": "public, max-age=86400"
  };
}