A self-hostable web app to manage your Yu-Gi-Oh! collection, build decks with the cards you actually own, see which meta decks you can put together (and what the missing cards cost), and learn how to play them.
Available in English, French, German, Italian and Portuguese — interface, card names and effects, and generated play guides.
- Accounts — multi-user, Argon2id passwords, short-lived JWT + rotated refresh token in an httpOnly cookie (or in the response body for native clients).
- iOS app — native SwiftUI client with a card scanner (separate
ygo-deckbuilder-iOSrepository). - Card catalog — fully synced from YGOPRODeck: names and effects in 5 languages, prints, rarities, Cardmarket prices. Typo-tolerant search in any language, or by the code printed on the card (
SDBE-EN001). - Collection — by print, condition, language and 1st edition, with an estimated value. Add a whole product (structure deck, tin, box…) from a gallery of HD box art, with the official quantities of each card (Yugipedia set lists).
- Products — every product you added, with its full content to rebuild it (what is missing, a copyable list), one-click deck creation, and a play guide for structure and starter decks.
- Deck builder — search limited to your cards (or the whole catalog), live validation (40–60 cards, 3 copies, banlist, Extra Deck), missing copies flagged, auto-save,
.ydkimport/export. Clicking a card opens its details with +/− per zone. - Card interactions — on every card: what it searches, summons or uses as material, and which cards search, summon or use it. Your cards first, click to jump from card to card.
- Automatic meta — recent tournament top decks are clustered into archetypes by content, with a consensus list, flex cards, a tier and the meta share.
- Deck suggestions & generation — for each meta deck, your coverage and the cost to complete it; one-click generation of the full meta list or an "owned cards only" version; auto decks for any archetype you own; official preconstructed decks (structure decks, starters, the decks inside boxes such as Legendary 5D's Decks or the 2-Player Starter Set) with what you own of each; a list of complete, playable decks built only from your collection, ranked by a solidity score. Missing cards go to the wishlist.
- Synergy & play guides — card effects are read (who searches, summons or sends what; starters, extenders, hand traps; which Extra Deck monsters are actually reachable). Generated decks favour cards that work together, and every deck gets a play guide: game plan, key cards, step-by-step combos, going first/second, mistakes to avoid. Optionally rewritten by an AI model (OpenAI-compatible or Anthropic, local models welcome).
- Duel simulator — test your decks with the real card effects: the EDOPro engine (compiled to WebAssembly) and the Project Ignis card scripts run on the server. Force your opening hand, set up the opponent's board (monsters, set cards, hand traps, graveyard), then play against a passive opponent (combo testing), make the opponent's choices yourself (interactions), or face the bot, which plays any deck on its own: it builds its board, attacks when the battle favours it and answers your plays with its hand traps and traps. Every summon type, chains, battle and the duel log are supported; you are only asked to chain after an opponent's action (or switch to every chain window, as in EDOPro). Every action is replayed like a cut-scene: summon and activation cut-ins, attacks, floating damage with screen shake, rolling LP, turn banners, synthesized sound effects (Web Audio, no audio files), victory confetti — with a skip button and a speed setting (normal / fast / off).
- Rules reminder — the official rules (current Master Rule) in 5 languages: turn structure, every summon type (Fusion, Ritual, Synchro, Xyz, Pendulum, Link), Extra Monster Zones, chains, battle.
- Wishlist — target print, max budget, priority, link to the deck that needs it, "Got it" moves it to the collection.
| Layer | Choice |
|---|---|
| Monorepo | pnpm workspaces + Turborepo |
| Web | Next.js 16 (App Router), React 19, Tailwind 4, TanStack Query, next-intl |
| API | NestJS 11 (Express), zod |
| Database | PostgreSQL 17 + Prisma 7 (pg driver adapter) |
| Shared contracts | packages/shared: zod schemas, DTO types, deck rules, locales — used by both web and API |
| Deployment | Docker Compose (db + api + web), a single exposed port |
apps/
api/ NestJS
prisma/ schema + migrations
src/
common/ prisma, guards (global JWT, roles), zod pipe, mappers, i18n, search
config/ env validated at startup
cli/ one-off scripts (card sync, meta sync, interaction index)
modules/
auth/ register, login, refresh, logout, me
cards/ catalog search, card details, archetypes, products
catalog-sync/ YGOPRODeck + Yugipedia clients, scheduled sync, product covers
collection/ CRUD, stats, OwnershipService
products/ product import, official quantities, owned products
decks/ CRUD, .ydk import/export, validation
wishlist/
meta-decks/ tournament lists, meta engine (engine/), sync
suggestions/ coverage, deck generation, playable decks
synergy/ effect reader, synergy graph, combos, guides (engine/), card interactions
duel/ duel simulator: EDOPro engine (WebAssembly), card data/scripts (data/), prompts & log (engine/)
web/ Next.js
messages/ UI translations, one folder per language
src/
app/ routes: (auth) and (app)
components/ ui/ (in-house design system), cards/, products/, layout/
features/ one view per domain (catalog, collection, deck-builder, guide…)
i18n/ next-intl setup (cookie, then browser language)
lib/api/ HTTP client + TanStack Query hooks per domain
packages/
shared/ web/API contracts
tsconfig/ shared TS configs
The browser only talks to the web app: /api/* is proxied by Next to the API. First-party cookies,
no CORS, a single domain to expose.
Native clients (the iOS app) use the same /api/* routes with the X-Auth-Mode: token header:
/auth/login, /auth/register and /auth/refresh then return { user, accessToken, refreshToken, … }
instead of setting cookies, the access token goes in Authorization: Bearer …, and the refresh token is
sent in the body of /auth/refresh and /auth/logout. Refresh tokens rotate; reusing one revokes the session.
Requirements: Node 22+, Docker (for Postgres), corepack enable.
pnpm install
pnpm db:up # Postgres on :5432
cp apps/api/.env.example apps/api/.env # set a real JWT_ACCESS_SECRET (openssl rand -base64 48)
cp apps/web/.env.example apps/web/.env
pnpm db:migrate
pnpm cards:sync # ~13,000 cards in 5 languages, a few minutes
pnpm dev # web :3000, api :4000Put your email in ADMIN_EMAIL before creating your account to get the admin role.
cp .env.example .env # fill POSTGRES_PASSWORD, JWT_ACCESS_SECRET, PUBLIC_URL, ADMIN_EMAIL
docker compose up -d --build- Migrations run when the API starts.
- On first start, the empty catalog triggers an automatic sync (
docker compose logs -f api). - HD product covers and official product quantities come from Yugipedia (
PRODUCT_COVERS_ENABLED). - The sync then runs on
CARD_SYNC_CRON(Mondays 4am by default) and does nothing if YGOPRODeck has not changed. - Put an HTTPS reverse proxy in front of port 3000 (see
Caddyfile.example). Over plain HTTP, setCOOKIE_SECURE=false.
.github/workflows/docker.yml publishes two public images to GHCR on every push to main and on
every v* tag: ghcr.io/mathieudubart/ygo-deckbuilder-api and …-web (tags latest, the version,
and the short commit sha). After the first run, switch both packages to Public in the
repository's Packages tab.
In Coolify: + New → Docker Compose, paste docker-compose.coolify.yml,
deploy. Coolify generates the domain (service web, port 3000), the Postgres password and the JWT
secret; the only variable worth setting is ADMIN_EMAIL (that account becomes admin when it signs up).
Nothing is exposed but the web service: the API and the database stay on the internal network. The
catalog syncs itself on first start, and the duel engine downloads its data into the ygo-duel volume.
To build on the server instead of pulling the images, point Coolify at this repository with
docker-compose.yml.
- The web image bakes
API_URL=http://api:4000at build time (Next serialises its rewrites), so the API service has to keep the nameapi. - Running a fork? Build with
--build-arg SOURCE_URL=<your fork>so the duel page links to your source (AGPL-3.0). - The published images are amd64. For an arm64 server, add
platforms: linux/amd64,linux/arm64to the build step of the workflow.
The duel engine needs the card scripts and databases from Project Ignis (~60 MB). The API downloads
them on first start into DUEL_DATA_DIR (a Docker volume, duel-data) and refreshes them on
DUEL_DATA_CRON (Tuesday 3 am by default); the page shows a waiting state until they are ready.
Each duel runs in memory on the API (a few MB); DUEL_MAX_SESSIONS caps simultaneous duels and idle
duels are closed after DUEL_IDLE_MINUTES. DUEL_ENABLED=false turns the simulator off.
There is no WebSocket: every answer to the engine is a plain HTTP request that returns the new state, the log events and the next choice, so the simulator works behind any reverse proxy.
If you run a modified version, set SOURCE_URL to your fork (AGPL-3.0, see License).
English (default), French, German, Italian and Portuguese. The language comes from the NEXT_LOCALE
cookie set by the language switcher, then from the browser's Accept-Language, then English — for
the web UI and for everything the API generates (card names and effects, guides, notes, errors).
Card texts come from YGOPRODeck (French in Card.nameFr/descFr, German/Italian/Portuguese in
CardTranslation). UI strings live in apps/web/messages/<locale>/<namespace>.json; English is the
reference and missing keys fall back to it.
The meta is computed from the tournament decklists published by YGOPRODeck (Tournament Meta Decks):
- the ~300 most recent lists are fetched (
META_SYNC_PAGES), alternate artworks are resolved; - they are clustered into archetypes by content (Jaccard on the cards played), not by name;
- for each archetype: a consensus list (cards by inclusion rate, at their most played copy count), flex cards, a tier based on meta share;
- staples (cards played across archetypes: hand traps, board breakers) are used to complete decks generated from your collection.
It runs on first start, then on META_SYNC_CRON (Mondays 5am), or manually with pnpm meta:sync or
the "Update" button on the Suggestions page (admin). The engine
(apps/api/src/modules/meta-decks/engine/) is made of pure functions tested on real lists. Meta decks
can also be imported from a .ydk file (POST /api/meta-decks/import-ydk, admin).
apps/api/src/modules/synergy/engine/ reads the official card text (Konami's PSCT format):
- effects: searches, special summons from the Deck / hand / GY, sends to the GY, triggers (Normal Summon, sent to the GY, End Phase…), "discard this card" costs;
- graph: A → B when an effect of A can search / summon / send B in this deck;
- roles: starter, extender, searcher, hand trap, interruption, removal, draw, boss;
- Extra Deck: Fusion / Synchro / Xyz / Link materials checked against the Main Deck;
- combos: a simplified simulation from 1–2 card hands to an end-of-turn boss.
Deck generation uses it (cards ranked by affinity with the deck core, unreachable Extra Deck monsters
removed, a synergy component and a minimum number of starters in the solidity score), and
POST /api/suggestions/guide produces the play guide shown under generated decks, in the deck
builder and on product pages.
Optional AI to write the guide (the computed guide stays the baseline and the fallback):
AI_PROVIDER=openai # any OpenAI-compatible API: OpenAI, Mistral, Groq, LM Studio, Ollama…
AI_BASE_URL=http://localhost:1234/v1 # e.g. LM Studio (from Docker: http://host.docker.internal:1234/v1)
AI_MODEL=qwen2.5-14b-instruct
# or: AI_PROVIDER=anthropic, AI_API_KEY=…, AI_MODEL=…Answers are validated with zod and cached in the database (DeckGuideCache) per list, model and
language. Writing happens in the background (one at a time) and the UI polls every 3 s, so there are no
proxy timeouts even with a slow local model. API-side limit: AI_TIMEOUT_MS (3 min by default).
Card interactions: after each catalog sync, the precise targets of every card (a quoted name or
archetype, or a tight filter such as "Level 1 LIGHT Tuner") are indexed in CardEffectTarget (a few
seconds for the whole catalog). A card page computes its own targets on the fly and queries the index
for the reverse question (GET /api/cards/:id/interactions). Generic effects ("1 monster") are not
indexed: they target everything. Manual rebuild: pnpm interactions:index (automatic on start when the
text reader changes).
Adding a product records it (OwnedProduct) in addition to its cards. YGOPRODeck lists each card once
per product, so the official copy counts are read on demand from the Yugipedia Set Card Lists and
stored in CardPrint.setQuantity (1 copy per card when unknown). The product page shows what is still
in your collection, a copyable list to rebuild it, and — for structure and starter decks — the play
guide and one-click deck creation.
The same set lists feed the official decks: every section of a product page titled "… Deck"
becomes a ProductDeck (a structure deck is one deck, Legendary Decks II is three). A daily job
reads the structure decks, starters and boxes that have not been read yet. The Suggestions page lists
them with your coverage (filter: structure / starter / box) and builds them from the official list or
from your cards only.
pnpm typecheck
pnpm test # deck rules, .ydk, search, locales, YGOPRODeck/Yugipedia parsing, meta engine, synergy engine, product quantities, duel engine bridge
pnpm build- Admin page: meta deck import, sync status
- Synergy engine (roles, card links, reachable Extra Deck, combos) + play guides
- Real structure deck quantities (Yugipedia set lists)
- Five languages
- Duel simulator (EDOPro engine) + rules reminder
- Bot opponent in the duel simulator (built-in heuristic AI, any deck)
- Duel simulator in the iOS app
- Opening-hand simulator (odds of opening each combo)
- Deck builder suggestions from the interaction index (owned cards linked to the deck, beyond its archetype)
- Shareable read-only public decks
- End-to-end tests (Playwright) in CI
AGPL-3.0-or-later. The duel simulator embeds the EDOPro engine (ygopro-core, AGPL-3.0),
so the whole application is distributed under the same license: if you run a modified version on a
server, you must offer its source code to its users (the duel page links to it through SOURCE_URL).
Card data and images: YGOPRODeck. Product box art and set lists: Yugipedia. Duel engine: ygopro-core (EDOPro, AGPL-3.0) through ocgcore-wasm (MIT); card scripts and databases: Project Ignis (CardScripts, BabelCDB). Yu-Gi-Oh! is a trademark of Konami; this project is not affiliated with or endorsed by Konami.