masi a07522a189 MAGIK — Commander deck builder
React 19 SPA + node:http/node:sqlite API in one container. Card data is
baked into the image from a Scryfall bulk build, so the runtime never calls
Scryfall. No server-side user state: decks live in the browser and travel in
the share URL, which is why there is no PVC and replicas: 1 is correct.

Adds a README covering the architecture and the reasoning behind it, and
publishes the project skills under .claude/skills/.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-01 21:12:19 -03:00
2026-10-01 21:12:19 -03:00
2026-10-01 21:12:19 -03:00
2026-10-01 21:12:19 -03:00
2026-10-01 21:12:19 -03:00
2026-10-01 21:12:19 -03:00
2026-10-01 21:12:19 -03:00
2026-10-01 21:12:19 -03:00
2026-10-01 21:12:19 -03:00
2026-10-01 21:12:19 -03:00
2026-10-01 21:12:19 -03:00
2026-10-01 21:12:19 -03:00
2026-10-01 21:12:19 -03:00
2026-10-01 21:12:19 -03:00
2026-10-01 21:12:19 -03:00

MAGIK

A Magic: The Gathering Commander (EDH) deck builder. Pick a Commander, build the 100-card list, and get legality checked live as you type — against real Scryfall data, with no account and no server-side state.

Live at https://magic.marcospaulo.dev.br

What it does

  • Deck builder — search Commander-legal cards, add them, watch the validator.
  • Legality engine — 100 cards, one Commander or a legal Partner pair, colour identity contained in the Commander, singleton except basic lands, no banned cards.
  • Share links — a deck is encoded into its own URL. There is no save button and no account; a deck is a link.
  • Browse / Analyze — browse the legal pool, and break a list down by mana curve, colour identity and type line.
  • Card art — hot-linked from Scryfall's CDN.

Architecture

Three pieces, and the boundaries between them are the design.

  Scryfall bulk download                (build time, host only)
  default_cards.json ──► scripts/build-cards.mjs ──► data/cards.db
                                                          │
                                                          │ baked into image
                                                          ▼
  ┌──────────────────────── one container, one process ────────────────────────┐
  │  server/index.mjs — node:http                                            │
  │    ├── /api/*     read-only queries over data/cards.db (node:sqlite)     │
  │    └── /*         static files from dist/, SPA shell fallback             │
  │  dist/            React 19 SPA, built by Vite                            │
  └──────────────────────────────────────────────────────────────────────────┘
                                                          │
                                          K3s: ClusterIP → Traefik
                                                          │
                                          Caddy on Oracle VPS, over Tailscale

The decisions behind it

One process, one container, no ORM. node:http and node:sqlite are built into Node 26. There is no Express, no Prisma, no native module to compile — the API image needs no build toolchain at all.

The card database is a build artifact, not a live dependency. Commander filtering is a one-off question about a static dataset, so it is answered once, ahead of time, into SQLite. The runtime never talks to Scryfall: no rate limits, no CORS, no upstream outage. The trade is that the index goes stale when Wizards publishes a Banned & Restricted List change; pnpm cards:build rebuilds it and that is the whole refresh story. See .claude/skills/commander-card-index.

No user state, therefore no PVC. Decks live in the browser and travel in the share URL. The service holds nothing, so a new release replaces the old one outright with nothing to migrate or restore. That is also why replicas: 1 is correct rather than a scaling compromise.

Rules come from data, not from a hardcoded list. Ban status and per-card colour identity are read from Scryfall, so the Commander Committee changing the ban list is a data refresh, not a code change.

API

All endpoints are read-only GET.

Endpoint Purpose
/api/health liveness + card count
/api/search filter the legal pool (name, color, type, cmc, commander, …)
/api/commanders search restricted to Commander-eligible cards
/api/cards?id=… batch fetch, repeated param, max 400
/api/cards/:uuid single card
/api/lookup?name=… resolve by name, repeated param

Layout

server/         node:http API + static serving; db.mjs is the card-index reader
src/lib/        commander.js is the rules engine; deckStore.js owns decks
src/components/ card browser, detail modal, commander picker, validator
src/routes/     Decks, DeckBuilder, Browse, Analyze, Settings
scripts/        build-cards.mjs, deploy.sh
deploy/k8s/     Namespace + Deployment + Service
templates/      project scaffolding
.claude/skills/ project-specific agent skills (see below)

commander.js and deckStore.js are plain JavaScript on purpose — node --test exercises them directly, and the UI imports the same file. There is no second copy of the rules.

Running it

pnpm install
pnpm cards:build     # downloads Scryfall bulk, builds data/cards.db (~1 min)
pnpm dev             # vite on :5173, api on :8099
pnpm test            # 35 tests over the rules engine and deck/store handling
pnpm typecheck
pnpm build
pnpm api             # serve the built SPA + API on one port

Deploy to the homelab cluster with scripts/deploy.sh (see .claude/skills/homelab-web-deploy).

Project skills

.claude/skills/ holds the project-specific knowledge that would otherwise live only in a chat log. They are the reusable part of building this thing:

Skill Owns
commander-card-index building, verifying and repairing data/cards.db; picking up B&R List changes
commander-deck-rules the legality engine in commander.js and deck/share handling in deckStore.js
homelab-web-deploy the Dockerfile / manifest / deploy-script pattern for a stateless web service on the K3s homelab

Status and history

PROGRESS.md is the working log: the stack decision with its rationale, the design direction, the task list, and the deploy history. Read it if you want the why rather than the what.

S
Description
Magic: The Gathering Commander (EDH) deck builder
Readme 17 MiB
Languages
TypeScript 47.3%
JavaScript 34%
CSS 16.6%
Shell 1%
Dockerfile 0.7%
Other 0.4%