A full-stack web app for Magic: The Gathering collectors to catalog the cards they own and add new ones by scanning them with a camera — at its core, a card scanner and collection tracker.
Status: Portfolio project, actively developed. Scoped for a small user base (Commander-first), with maintainability and clean architecture as the primary goals.
The two core features:
- Collection tracking — add, edit, and remove cards with per-copy quantity, finish (non-foil / foil / etched), condition, language, and notes. Import and export via CSV or pasted lists (Moxfield / Archidekt compatible).
- Card scanner — identify physical cards through your device camera using an OpenCV + OCR pipeline and add them straight to your collection.
Supporting the collection:
- Card search — search the full MTG catalog (~96k cards) by name, oracle text, mana cost, color, type, rarity, and set, with representative-printing selection and infinite scroll — to find cards to add.
- Card detail view — every printing, oracle text, legalities, finishes, live prices, high-res imagery, and a price-history chart.
- Watchlist — track cards or specific printings with target prices and price trends.
- Dashboard — total collection value, value-over-time trend, value by set, 24h price movers, and set-completion progress.
- Pricing — current and ~90 days of historical prices per finish, synced daily from MTGJSON via a scheduled job.
- Accounts — email/password auth (argon2), Google sign-in, profiles with avatars.
Frontend — React 19 · Vite · React Router · Zustand · TanStack Query · Tailwind CSS · Radix UI / shadcn · Recharts · OpenCV.js + Tesseract.js (scanner)
Backend — NestJS · Prisma · PostgreSQL · Passport (JWT + Google OAuth) · argon2 · MinIO (S3-compatible avatar storage) · Swagger/OpenAPI
Tooling — Docker Compose · GitHub Actions (CI + scheduled price sync) · ESLint · Prettier · Jest · Vitest
Monorepo with independent frontend/ and backend/ npm projects:
MagicCollectionManager/
├── frontend/ React + Vite SPA
│ └── src/
│ ├── pages/ route screens
│ ├── components/ UI
│ ├── hooks/ data-fetching + logic (per domain)
│ ├── services/ thin API-client wrappers (per domain)
│ ├── stores/ Zustand client state
│ └── lib/ adapters, helpers, scanner vision, import parsers
├── backend/ NestJS API
│ ├── src/ one module per feature (auth, cards, collection,
│ │ watchlist, prices, dashboard, users, …)
│ └── prisma/ schema + migrations + seeds
├── scripts/ standalone jobs (MTGJSON price sync)
├── docker-compose.yml
└── .github/workflows/ CI + daily price sync
Request flow (backend): HTTP controller → service (business logic) → Prisma → Postgres.
- Node 22 (see
.nvmrc) - Docker + Docker Compose
Copy the backend env template and fill in the values:
cp backend/.env.example backend/.envAt minimum set a strong JWT_SECRET. Google OAuth (GOOGLE_CLIENT_ID /
GOOGLE_CLIENT_SECRET) is optional — leave blank to disable Google sign-in. The default
DATABASE_URL and MinIO values already match the Docker Compose services below.
The quickest path — starts Postgres, MinIO, the backend, and the frontend together:
docker compose up --build- Frontend → http://localhost:5173
- Backend API → http://localhost:3000
- API docs (Swagger) → http://localhost:3000/docs
- MinIO console → http://localhost:9001
Run migrations and seed the card catalog (imports MTG cards from Scryfall bulk data):
cd backend
npm run prisma:migrate # apply schema
npm run prisma:seed # seed reference data (tags, etc.)
npm run cards:seed # import the card catalog from Scryfall (~96k cards)The card import is large and runs once. Price history is populated separately by the price-sync job (see below).
Run only Postgres + MinIO in Docker, and the apps directly for hot reload:
# infra only
docker compose up postgres minio
# backend
cd backend
npm install
npm run prisma:generate
npm run start:dev # http://localhost:3000
# frontend (separate terminal)
cd frontend
npm install
npm run dev # http://localhost:5173Backend (backend/)
| Command | Description |
|---|---|
npm run start:dev |
Start the API in watch mode |
npm run build |
Compile the API |
npm run test |
Run the Jest test suite |
npm run lint / type-check |
Lint / type-check |
npm run prisma:migrate |
Apply database migrations |
npm run cards:seed |
Import the card catalog from Scryfall |
Frontend (frontend/)
| Command | Description |
|---|---|
npm run dev |
Start the Vite dev server |
npm run build |
Production build |
npm run lint |
Lint |
npm run format:check |
Check formatting |
npm run test:run |
Run the Vitest suite |
scripts/sync-prices.mjs pulls market prices from MTGJSON and maintains a rolling
~90-day daily history per card/finish. It runs automatically via GitHub Actions
(.github/workflows/sync-prices.yml) on a daily cron (15:00 UTC), and can be run manually:
node scripts/sync-prices.mjs.github/workflows/ci.yml runs on every pull request and push to main, with
path-filtered jobs so frontend and backend only run when their code changes:
- frontend-ci — lint, format check, tests, build
- backend-ci — lint, type-check, then tests against a real
postgres:16-alpineservice container (prisma migrate deploy), and build
- Scryfall — card metadata and imagery (imported into the local catalog).
- MTGJSON — market pricing data.
Prices are indicative estimates, refreshed roughly daily — MysticArchive is a tracking tool, not a financial or trading service.
This is a personal portfolio project. All Magic: The Gathering card data, names, and imagery are property of Wizards of the Coast; this project is unofficial and not affiliated with or endorsed by Wizards of the Coast.