Derek Gee
tV

tcgVault

In production

Inventory, pricing, and set tracking for a trading-card collection. It is the vault I actually use, which is why it is the one that has broken in the most instructive ways.

StartedMarch 2026
Commits336
Tests2,332 unit · 24 e2e
Games3

Figures measured 2026-07-14.

What it does

the unglamorous parts are the real ones

A portfolio dashboard. Total value, cost basis, and unrealized profit and loss; breakdowns by item category and by game; top movers; and a value-over-time chart built from a real daily history rather than a single live lookup.

Three genuinely different kinds of item. Raw singles, graded singles (which carry a grading company, a grade, and a certificate number), and sealed product. They price differently and they behave differently, so each carries its own purchase price and its own profit and loss.

Pricing that runs itself. A nightly job snapshots every price so the portfolio has genuine history. Behind it: a 24-hour cache, a stale-data indicator that says so plainly when the job hasn't run, and a circuit breaker that stops the app hammering the price vendor once it starts refusing requests. That last one exists because an earlier version got me rate-limited.

Set completion tracking. Checklists per set, progress as you fill them, what's still missing, and what it would cost to finish.

Also, in less detail:

  • Price alerts, above or below a target — including on cards I don't own.
  • Physical storage locations, with quantities allocated across them.
  • Duplicate detection.
  • Adding a card by pasting a marketplace URL, including uncataloged and Japanese cards.
  • Sealed-product autocomplete and MSRP auto-fill.
  • Three games — Pokémon, One Piece, and Disney Lorcana — though only Pokémon has live API pricing behind it.

Currently building: a seller-channel importer that reads my own marketplace listings back in and flags the ones whose prices have drifted away from the market. Three of its seven subtasks are done.

Walkthrough

real app · invented data

These are screenshots of the running app, captured against a seeded demo database. Every card, price, and total below was invented for the purpose — none of it is my collection.

The tcgVault dashboard: total value, cost basis and unrealized profit-and-loss cards above a portfolio value line chart, with breakdowns by item category and by game beneath it.
The dashboard — totals, the value chart, and breakdowns by category and game. Demo data — fictional inventory
The tcgVault inventory table, beneath a row of filter controls: raw singles, graded singles and sealed products interleaved across three games, each row showing purchase price, current value, profit and loss, quantity, and purchase date.
Inventory — all three item types, filterable and sortable, with per-item profit and loss. Demo data — fictional inventory
The detail page for a graded single: the card image beside a details panel listing grading company PSA, grade 10 and a certificate number, above a pricing panel showing purchase price, current value and profit.
A graded single — grading company, grade, and certificate number. Demo data — fictional inventory
The set tracker for Paradox Rift, showing a three-of-four-collected badge and a checklist of cards with their normal and holofoil variants marked as owned or missing.
Set tracking — a checklist per set, with what's owned and what isn't. Demo data — fictional inventory

How it's built

boring on purpose
FrontendNext.js · React · TypeScript
DataPostgreSQL
TestsVitest · Playwright
Runs onself-hosted app · managed Postgres
Sourceprivate repo

The repo is private because its history is. Four months of commits carry real configuration and real values, and scrubbing a git history properly is its own project — one I haven't done yet. So the honest answer is that the code isn't public because the past of it isn't, not because there's anything clever in it worth hiding. The app is above.

Every change goes through the same pipeline — planned, built, reviewed by two independent AI reviewers and an automated reviewer on the pull request, then merged. That process is a project in its own right, and it has its own page.

What I learned

two that cost me something

The importer that would have worked right up until the first expensive card. I wrote a CSV importer to pull in listings from a marketplace export. It parsed prices with a function that silently gives up on any number containing a thousands separator — and one bad row aborted the whole file. Every test passed, because every card in the sample export I built the fixtures from was cheap enough that no price ever reached four figures. The bug was sitting there waiting for the first $1,000 card. A reviewer caught it; the test suite never would have. A green suite proves your code handles the reality your fixtures came from — not the reality it's about to meet.

The nightly job that reported success for a day while doing nothing. For a day, the nightly price snapshot failed every time it ran — and the run log recorded a success every time. It was logging that the job had been handed off, not that it had finished. So a run that died instantly wrote a clean "success" straight away, while a run that actually worked outlived the window the log was written in and left no record at all. Broken looked healthier than working. Monitoring the wrong thing is worse than not monitoring, because it sells you confidence you haven't earned.

Next in the fleet

the other three vaults