- TypeScript 80.7%
- JavaScript 16.3%
- Dockerfile 2.2%
- CSS 0.8%
| .forgejo/workflows | ||
| app | ||
| data | ||
| lib | ||
| prisma | ||
| public | ||
| scripts | ||
| .gitignore | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| compose.yaml | ||
| Dockerfile | ||
| eslint.config.mjs | ||
| next.config.ts | ||
| package-lock.json | ||
| package.json | ||
| postcss.config.mjs | ||
| prisma.config.ts | ||
| README.md | ||
| tsconfig.json | ||
dex-binder
A Gen 1 Pokémon TCG binder tracker focused on Japanese cards. Browse all 151 original Pokémon in a book-style binder, pick the exact Japanese card you own from a visual card browser, and track your collection — with offline support via PWA.
Features
- Book-style binder — 3×3 card grid per page, paginated as a two-page spread
- Japanese card browser — 400+ Japanese cards indexed from modern SV-era sets via TCGDex
- Custom card entry — paste any image URL for cards not in the index (e.g. older or obscure sets)
- German Pokémon names — fetched from PokéAPI and cached server-side
- Search — find any Pokémon by German name, English name, or Pokédex number
- S3-compatible image storage — selected card images are downloaded and saved to an S3-compatible bucket on selection, so the binder keeps working even if the source (TCGDex, pokemontcg.io, a pasted URL) later renames or removes the file
- Offline support — IndexedDB cache + PWA service worker
- Persistent storage — SQLite via Prisma, survives restarts
Stack
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router, TypeScript) |
| Styling | Tailwind CSS |
| Database | SQLite via Prisma v7 + better-sqlite3 |
| Card data | TCGDex Japanese API (static seed) |
| Pokémon names | PokéAPI |
| Offline cache | IndexedDB (idb) |
| PWA | Custom service worker + Web App Manifest |
Getting Started
Prerequisites
- Node.js 24+
- npm
Local development
npm install
npx prisma migrate dev
node scripts/seed-jp-cards.js # builds data/gen1-jp-cards.json (~434 cards)
npm run dev
Open http://localhost:3000.
Re-seeding card data
The card index in data/gen1-jp-cards.json is a static snapshot built from TCGDex. Re-run the seed script whenever you want to pick up newly indexed sets:
node scripts/seed-jp-cards.js
API
| Method | Path | Description |
|---|---|---|
GET |
/api/health |
Health check ({"status":"ok","database":"connected"}, 503 if the DB check fails) |
GET |
/api/version |
Git commit hash baked in at build time ({"version":"<hash>"}, "unknown" outside Docker) |
GET |
/api/cards |
Japanese card index lookup |
GET/POST |
/api/slots |
Binder slot data |
GET/PATCH/DELETE |
/api/slots/:id |
Manage a single binder slot |
GET |
/api/card-images/:filename |
Proxy a card image out of the S3-compatible bucket |
Usage
- Click a card slot to mark it as done — opens the Japanese card picker
- Right-click a completed slot to change the card
- Click a completed slot again to unmark it
- Use the search bar to jump to any Pokémon by name or
#number - If a card isn't in the index, use the "Custom card" section at the bottom of the picker to paste any image URL directly
Deployment
Docker Compose
docker compose up --build
The SQLite database is stored in a named Docker volume (db-data) mounted at /data/dex-binder.db inside the container. Migrations run automatically on startup. The image build bakes in the git commit hash (git rev-parse --short HEAD), exposed at runtime via /api/version.
Environment variables
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
file:./dev.db |
SQLite path (file:<path>) |
S3_ENDPOINT |
— (required) | S3-compatible endpoint URL |
S3_REGION |
eu-central-1 |
Region passed to the S3 client (some self-hosted S3-compatible stores ignore the value but still require one) |
S3_BUCKET |
— (required) | Bucket card images are stored in |
S3_ACCESS_KEY_ID |
— (required) | Access key for S3_BUCKET |
S3_SECRET_ACCESS_KEY |
— (required) | Secret key for S3_BUCKET |
DOMAIN |
dex-binder.localhost |
Hostname Traefik routes to this app |
Image storage
Card images are stored in an S3-compatible bucket rather than on disk, so the app container stays stateless (aside from the SQLite DB) and images survive redeploys/rebuilds. Point S3_ENDPOINT/S3_BUCKET/S3_ACCESS_KEY_ID/S3_SECRET_ACCESS_KEY at any S3-compatible store reachable from the app container.
Access control (mTLS)
Instead of Basic Auth, the router requires a valid client certificate signed by a shared personal CA — no password prompt, and no per-app re-auth on installed iOS PWAs (which otherwise lose the Basic Auth cache on every relaunch). The CA, per-device certificate issuing scripts, and the actual TLS Options definition (with clientAuth) live in the separate mtls-ca repo, since this is shared across apps. Docker labels can only reference a tls.options entry, not define one with clientAuth settings — that requires Traefik's file provider — so this repo's router label points at it via the @file suffix: traefik.http.routers.dex-binder.tls.options=dex-binder-mtls@file. Any device certificate issued from that CA works for both dex-binder and haushaltsbuch — no need to issue a separate certificate per app.
CI
Forgejo Actions workflow at .forgejo/workflows/ci.yaml runs on every push to main and every PR:
- build —
npm ci,npx prisma generate,npm run lint,npm run build. - docker —
docker build ., so a broken image build fails the workflow. - deploy — only on pushes to
main, after the above pass: POSTs to a webhook (secrets.DEPLOY_WEBHOOK_URL) so Portainer re-pulls this repo and rebuilds/redeploys the stack.