← Christian Bellofatto

Runebinder

Astro · Cloudflare Workers · Supabase Postgres · live site

Runebinder does three jobs. It tracks a collection across two games — every card, every printing, and what the whole thing is worth as prices move. It turns that inventory into deck building, so I can see which cards are already spoken for and which decks are actually sleeveable tonight. And it follows every online order from checkout to the binder, so nothing gets quietly dropped somewhere between a ten-seller checkout and a pile of envelopes.

Getting cards in used to be the thing that killed it. Typing names off a stack of cardboard is exactly the chore that kills a collection database within a month — you skip it once, the data goes stale, and then it's wrong forever. Now I photograph the pile and Claude reads it, resolving names, counting duplicates, and matching printings before writing the rows. Orders work the same way: a Claude skill sweeps my email for confirmations and shipping notices and reconciles them against what's outstanding, so the collection knows what's on a truck somewhere without me telling it.

Deck viewer

Runebinder deck viewer showing Master Yi — Tempered laid out by zone: legend, chosen champion, battlefields, runes, main deck, and sideboard, with a 100% built indicator and $307.08 owned value
A deck laid out the way the game does — legend, chosen champion, battlefields, runes, main deck, sideboard — with build completeness and owned value in the corner.

The question this page answers is "can I sleeve this deck tonight?" — which is harder than it sounds, because the same physical cards get spoken for by several decks at once. Availability is allocated in priority order, and decks marked as variants of each other are treated as alternatives rather than both reserving the pile. A deck can show as complete without me owning enough copies to build it and its sibling simultaneously, which is correct: I only ever sleeve one.

Dashboard

Runebinder dashboard showing 1,877 cards across 525 unique, a 35-day collection value chart, value split by domain, six arriving orders, and top holdings by value
1,877 cards across 525 unique names, valued nightly against TCGplayer market prices.

A nightly job pulls prices into a snapshot table, so the value line is real history rather than a number recomputed on each page load. The dashboard reads that history, splits the collection by domain, and surfaces the two things I actually act on: what's arriving, and how little it would cost to finish the current buy list.

Orders

Runebinder orders page listing 106 seller orders across 20 checkouts, with per-vendor rows showing which decks each order feeds, card counts, shipping status, and cost
106 seller orders out of 20 checkouts, each row showing which decks it feeds.

Buying singles means one checkout explodes into a dozen seller orders that ship separately and arrive over three weeks. This page models that grain directly — one row per seller sub-order — and attributes each one back to the decks it was bought for. Cards in transit count as on-order, so the buy list doesn't tell me to re-purchase something already in a padded envelope.

How it's built

The site itself is strictly read-only: every mutation runs through Claude, which holds the write credentials, and the web app only renders what's there. That split is deliberate — the agent is good at messy input, and a surface that can't write can't corrupt the collection when I'm tired and clicking fast.

Astro rendering per request in a Cloudflare Worker, with no client framework — interactivity is a handful of inline scripts. The browser reads Supabase Postgres directly over PostgREST with a publishable key, so there's no API layer of my own to maintain. That's only safe because the database was hardened for it: the anonymous role holds SELECT and nothing else, every readable table carries a select-only policy, views run as the invoker so they can't tunnel around those policies, and the audit tables aren't granted at all.

Ten tables hold facts and eight views derive everything else, which was a deliberate correction. The version of this that lived in Notion stored derived counts by hand, and they drifted — the collection would confidently report that a deck was complete when it wasn't. Facts in tables, everything computed in views, and the drift stops.