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>
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.