A free, open-source, self-hostable website for finding any trading-card-game card in every major language and printing playtest proxies at the fixed competitive card size (63x88 mm). Non-commercial; donations only.
The npm packages are scoped
@proxyforge/*internally (the old working codename); the public project name is Proxy Printer.
Status: Phases 0-4 done, plus the Meilisearch search backend. Foundations and the TCGdex data spine (Phase 1), the image pipeline (Phase 2: best per-language sourcing + fetch-into-storage with honest measured DPI), the browse/detail/print web UI (Phase 3), the dual-mode print engine (Phase 4: home PDF + MakePlayingCards ZIP), and a Meilisearch index (typo-tolerant, multilingual) with an automatic Postgres-FTS fallback. See
docs/ARCHITECTURE.mdfor the full design and the Roadmap below for what is left.
Legal: this is a fan tool for personal, non-commercial playtesting. Proxies are not tournament-legal and may not be sold or passed off as genuine. Card art and text are copyright their respective owners; this project is not affiliated with, endorsed by, or sponsored by any rights holder. See
docs/ARCHITECTURE.mdsec.10. An attorney review is a hard gate before any public launch.
- SIZE is fixed: always 63x88 mm (the universal competitive size). Hardcoded.
- DPI is separate - it is image sharpness, the only quality dial.
744x1039 px@300 DPI,1488x2079 px@600 DPI for the same physical rectangle.
TCGdex (data spine) -> PostgreSQL 16 + pg_bigm -> SeaweedFS (storage) +
Meilisearch (search) + Valkey (queue) + imgproxy/sharp -> Next.js + Fastify,
behind Caddy. One docker compose on an Oracle Always-Free ARM box or a home
server. pokemontcg.io is a deprecated, default-OFF overlay (merged into paid
Scrydex), kept behind a swappable adapter.
Prereqs: Node >=22 and Docker. (Only Node is needed for unit tests.)
cp .env.example .env # then edit secrets in .env (gitignored)
npm install
# offline checks (no DB, no network)
npm run typecheck
npm test # pure-logic unit tests
npm run brand-lint && npm run gpl-check
# bring up Postgres + Meilisearch (builds the pg_bigm image the first time)
docker compose up -d postgres meilisearch
npm run migrate # applies db/schema.sql
# ingest the TCGdex spine. Start with a tiny dev slice:
npm run ingest -- backfill --langs en,ja,fr --limit-sets 2
# ...then the full multilingual backfill:
# point TCGDEX_BASE_URL at a self-hosted tcgdex clone first, then:
npm run ingest -- backfill --refresh-mv # add --full for rich per-card data
# build the search index, then start the UI:
npm run search -- reindex
npm run web:dev # http://localhost:3000Fetches the best per-language source into storage with real measured DPI. EN
upgrades to ~296 via the pokemontcg.io image CDN; other languages use TCGdex
(~242); honest DPI is recorded per image. Default storage is local FS
(data/images, $0); set STORAGE_BACKEND=s3 + IMAGES_DIR for production.
npm run images -- fetch --langs en,fr,de,it,es,pt,ja,ko,zh-cn,zh-tw
npm run images -- fetch --langs en --limit 50 # dev slice
npm run images -- fetch --no-en-hires # TCGdex-onlyThe card_display materialized view is the read-model; npm run search -- reindex
refreshes it and pushes every row into a Meilisearch index. With
SEARCH_BACKEND=meili (the default) the web app gets typo-tolerant, multilingual
ranking; if Meili is unreachable or the index is not built yet it transparently
falls back to the Postgres FTS + pg_bigm query, so the site never hard-fails.
npm run search -- reindex # refresh MV + (re)index all docs
npm run search -- reindex --langs en,ja # subset of languages
npm run search -- status # index health + document count
npm run search -- search "charizard" --lang en # debug a query from the CLIRe-run reindex after each ingest so the index tracks the catalog.
export IMAGES_DIR="$PWD/data/images" # share the image store with the renderer
npm run web:dev # http://localhost:3000
# or: npm run web:build && npm -w @proxyforge/web run startFaceted browse (set / language / type / promo + name search), card detail with a language switcher and DPI / English-fallback badges, a localStorage print list, and a one-click render to home PDF or MakePlayingCards ZIP (with paper, DPI, gutter, and bleed options). Search is served by Meilisearch with the Postgres fallback described above.
# from a print_list in the DB (size is always 63x88mm; choose paper/dpi/bleed):
npm run print -- pdf --list <uuid> --out deck.pdf --paper A4 --dpi 300 --bleed
npm run print -- mpc --list <uuid> --out deck.zip --dpi 300
# standalone demo from image URLs (no DB needed):
npm run print -- pdf --urls "https://assets.tcgdex.net/en/sv/sv03/004/high.png" --out demo.pdf --bleed- Home PDF: 3x3 N-up at fixed 63x88mm, optional 1/8in synthesized bleed, vector crop marks, A4/Letter, 300/600 DPI, ink-saver. Bleed requires A4 (nine full-bleed cards exceed US Letter height) - Letter+bleed auto-switches to A4.
- MPC ZIP: one 822x1122px (@300) PNG per card with asymmetric bleed + a
clean-room
order.xml(no GPL code) you feed to MakePlayingCards.
db/ canonical schema.sql (single source of truth) + pg_bigm Dockerfile + NOTES
docs/ ARCHITECTURE.md, TECH_STACK.md, OPEN_ITEMS.md
docker-compose.yml data-plane infra (postgres, valkey, meili, seaweedfs, imgproxy, caddy)
packages/config typed env + the compliance posture (single source of truth)
packages/db pg pool + migration runner (applies schema.sql)
packages/ingest the Phase-1 spine: SourceAdapter, TCGdex adapter, normalization,
set-matcher, idempotent upserts, backfill + incremental, CLI
packages/images the Phase-2 image pipeline: per-language source resolver,
fetch-into-storage (local FS / S3), honest DPI metadata, CLI
packages/search the Meilisearch read-path + indexer: fetch-based Meili client,
card_display -> document mapping, keyset reindex, query builder, CLI
packages/print the Phase-4 print engine: exact geometry (with gutter), sharp
image-prep + bleed synthesis, home PDF (pdf-lib), MPC ZIP, CLI
apps/web the Phase-3 Next.js UI: faceted browse, card detail + language
switcher, localStorage print list, render API, image route
scripts/ brand-lint + gpl-import-check (CI gates)
.github/workflows/ci.yml typecheck/test/lint + Postgres+pg_bigm migration smoke-test
Western languages (en, fr, de, it, es, pt) share TCGdex set IDs, so the same
physical card collapses into one card_print row with a card_localization
per language (natural key = card_set_id + normalized collector number).
Japanese, Korean, and Chinese use different set structures, so each becomes
its own card_print row (a genuinely distinct printing). English-image
fallback is derived in the read model when a localized scan is missing.
Done: Phase 0 (monorepo, config, db, CI), Phase 1 (TCGdex ingest), Phase 2 (image pipeline, local-FS storage), Phase 3 (web UI), Phase 4 (print engine), and the Meilisearch search backend with Postgres fallback.
Still open (roughly in priority order):
- Image coverage. Only cards with an image source are browseable; today that is ~72% of prints (the read-model excludes imageless cards). Close the gap with the two image sources below.
- JA native ~350 DPI scraper (pokemon-card.com) - the only native >300 DPI Japanese source. Fragile (per-card detail-page scraping); needs a circuit breaker + filename cache + EN fallback.
- malie.io EN hi-res path - load-bearing for the $0 English hi-res route since pokemontcg.io merged into paid Scrydex; availability and terms still unverified.
- Production serve plane - SeaweedFS object storage + imgproxy derivatives (today images are local-FS only).
- Search refinement - the index is currently a single Meili index with a
langfilter; per-language indexes with CJK tokenizers (the architecture's design) would improve ja/ko/zh recall. - Optional Real-ESRGAN upscaling (Phase 6) for low-DPI sources.
- KR / Simplified-Chinese image scarcity + watermarked-source legal review.
- Coverage dashboard + admin UI for the
card_print_reviewqueue (per-set / per-language cards-with-image vs without). - Attorney sign-off (HARD launch gate) - ephemeral-cache-as-hosting risk, image redistribution terms, KR watermarked art, donation copy.
- Public name + domain - the project is now "Proxy Printer"; the npm
scope (
@proxyforge/*) and a domain are still to be finalized.
See docs/OPEN_ITEMS.md for the full decision log behind these.