Skip to content

Contributing › Architecture

Architecture

spellcache is four containers and one external data source. The rules behind these choices, and the anti-patterns they rule out, are in Development.

Data flow between Scryfall, the worker, PostgreSQL, Valkey, the web app and the browser bulk data upserts jobs enqueue · cache queries HTTPS large images Scryfall bulk files · image CDN worker nightly import · revaluation Valkey job queue · locks · cache PostgreSQL 17 catalogue · user data web Next.js · thumbnail proxy Browser / Android through your proxy

Components

Component Role
web (apps/web) Next.js app: pages, Server Actions, sign-in, public share pages, thumbnail proxy. Applies the database migrations at start-up.
worker (apps/worker) Downloads Scryfall’s bulk files every night, upserts the catalogue and prices, then revalues every container. Consumes jobs queued by web (Settings › Administration › Jobs).
postgres One database: the rebuildable catalogue (cards, sets, card_prices, import_runs) and the irreplaceable user data.
redis (Valkey) Job queue between web and worker, the nightly lock, search cache and sign-in rate limits.
packages/core Scryfall client and schemas, image URLs, job queue contract. No Next.js dependency.
packages/db Drizzle schema, migrations, PostgreSQL client, shared test database.

Card data and images

  • The catalogue is a local mirror: no Scryfall API call is made during a user request, except two rate-limited server-side cases (a set released today, a card missing from the catalogue).
  • Small images and art crops go through the web thumbnail proxy, which stores them on the thumbnails volume on first request. Large images are loaded from Scryfall’s image CDN by the browser.
  • Image URLs come from the bulk data, never built by hand.

Data model

collections ─┬─ collection_members (user, role: owner | editor | viewer)
└─ containers (kind: collection | binder | deck | list)
├─ holdings (card, finish, condition, language, quantity, deck zone)
└─ container_stats (precomputed value)
  • A collection is a shared space, not an account’s property. Access always goes through collection_members; containers never point to a user.
  • Containers hold everything: the collection root, binders, decks and lists. A deck is a container whose holdings carry a zone (main, side, commander) and whose state moves through plan, assemble, built and dismantled.
  • container_stats stores each container’s value, recomputed by the worker after the nightly import, so a value banner reads one row.
  • Trades live in trades and trade_lines, and only touch holdings when they are applied.

Nightly run

  1. The worker’s scheduler (or the host’s cron entry) takes the spellcache:worker:lock:nightly lock in Valkey, so a night never runs twice.
  2. The bulk import downloads the latest files and upserts cards, sets and prices, recording the run in import_runs.
  3. The revaluation recomputes container_stats for every container from the new prices.

See Scheduled tasks for the host side.