Card Atlas

Live

A collection manager for sports and trading cards, running in production against a catalogue assembled from seventeen upstream data sources.

Role
Solo — product, engineering, and operations
Period
2026 – present
The problem
Collectors who own both a Charizard and a Wembanyama rookie have to keep two hobbies in two different tools, because no catalogue covers trading card games and sports at the same time.
The outcome
One catalogue spanning 22 trading card games and 9 sports — 473,000 base cards and 698,000 variants — with 220+ releases shipped behind ~1,860 automated tests.
~473K
Base cards catalogued
308
API routes
~1,860
Automated tests
220+
Releases shipped

The problem

Card collecting has split into two hobbies with two toolchains. Sports collectors use one set of trackers; trading card game players use another. Anyone who collects both — and plenty of people do — ends up maintaining their collection in two places, with two definitions of what a card is worth.

Building one catalogue that covers both means solving a data problem before you can solve a product problem.

The approach

React and TypeScript on the frontend, built with Vite and styled with Tailwind, using TanStack Query for server state. The backend is FastAPI on fully async SQLAlchemy against PostgreSQL, with Alembic managing 158 migrations across 53 tables. Authentication is JWT with OAuth2 through Google, GitHub and Discord, and payments run on Stripe across three tiers.

Deployment is split by workload: the static frontend and the Astro blog ship as a single artifact to a global edge network, the API runs as a managed container service, and catalogue images sit in object storage behind a CDN domain.

What was actually hard

Seventeen sources disagree about what a card is. One treats parallels as separate products; another treats them as attributes of a parent. Reconciling them into a single variant model that stays stable while those sources change their own schemas underneath you is the part that never really finishes — and the part that decides whether search results feel correct.

Scale showed up in the query planner, not the CPU. At a few hundred thousand rows, the difference between a query that uses an index and one that does not stops being academic. A meaningful share of the work has been finding N+1 patterns, adding the right composite indexes, and making pagination mandatory.

Shipping fast requires a net. Around 1,860 tests — roughly 1,459 backend and 400 frontend — run in CI on every push. At 220+ releases, that is the only reason shipping stays routine rather than frightening.

The outcome

Live in production with a catalogue of 22 trading card games and 9 sports: about 4,500 sets, 473,000 base cards and 698,000 distinct variants. Alongside the collection tools it runs a social layer — profiles, following, an activity feed, comments and a moderation queue — plus a badge system, a card-show directory and an editorial blog.

It operates on automated release, migration and rollback paths, a scheduled task system handling 58 recurring jobs, and a weekly security pipeline that scans dependencies, code, secrets and containers against a tracked baseline so only new findings surface.

Stack

  • React 19
  • TypeScript
  • Vite
  • Tailwind CSS
  • FastAPI
  • SQLAlchemy 2.0 async
  • PostgreSQL 16
  • Alembic
  • Stripe
  • Astro

Links