Skip to content
← All writing

Building an MTG Collection App, Part 1: The Collection

· 3 min read #mtg#side-projects#ai#engineering

Every Magic: The Gathering player hits the same moment eventually. You’re holding a card, sure you have a second copy somewhere, and the somewhere is four shoeboxes, two binders, and a deck box that may or may not belong to your kid.

I scan my cards with ManaBox, which is excellent at scanning and less good at answering questions like “what could I build with the stuff I already own?” So I did what any reasonable infrastructure person does when an app is 90% of what he wants. I built the other 10% myself, and it turned out to be about 400% of the work.

This is part 1 of 3. The plan was simple. Export the collection from ManaBox as a CSV, drop it in Google Drive, have a small web app read it, and store it somewhere I can query. Four steps. What could go wrong?

Day one: the database migration nobody asked for

The first commit used Supabase. The second commit, the same day, migrated off it to Neon Postgres and Auth.js. I’d like to say I ran a careful evaluation. Really, I already had several Supabase projects on the free tier and didn’t want this one to be the one that tipped me over. Migrating a database on day one is the cheapest a migration will ever be, and I recommend it the way I recommend getting your flu shot early.

The OAuth scope tango

To read a file out of Google Drive, you ask Google for permission. My first try asked for drive.readonly, which lets the app read your entire Drive. Google rightly calls that a sensitive scope. Using it means app verification, a scary “this app isn’t verified” screen, and a list of approved test users. For an app with exactly one user. Me.

The fix was the drive.file scope. The app only sees files you hand it yourself through Google’s file picker. It’s less access, needs no verification, and the warning screen goes away. It’s the rare case where the more secure option is also the less annoying one. I’ll be bringing this up at work the next time someone tells me security slows things down.

Then I picked a file and got: “File is not found.”

The file existed. I had just picked it. Two days later I learned that the picker needs your Google Cloud project number. Not the project ID, which looks just as official and sits right next to it on the same screen. Without the number, the picker never actually grants access to the file you chose. The error message said nothing about any of this.

Diffs, not overwrites

Every import is a diff. The app downloads the CSV, compares it to what’s already in the database, and shows me what was added, removed, and changed before anything gets written. It’s the same reason I want a terraform plan before a terraform apply. A bad scan should never quietly delete half my collection.

Card details like images, prices, and format legality come from Scryfall, which has a lovely API and takes 75 cards per request. Scryfall also has rate limits, which I respected eventually, after a commit titled “Respect Scryfall rate limits.” We don’t need to talk about the commits before that one.

Security for an audience of one

Only one person can use this app: me. The email allowlist is checked in three places: at sign-in, at the proxy, and in every API route. Is that overkill for a card collection? Probably. But I do security for a living, and I wasn’t going to be the guy whose Commander decks ended up in a breach report.

Where part 1 ended

By the end of it I had a searchable grid of every card I own, with filters for color, type, rarity, set, mana value, and format, plus prices. I could finally answer “do I have another one of these?” without emptying a shoebox onto the floor.

Which raised the obvious next question. If the app knows every card I own, couldn’t it build a deck for me?

That’s part 2.