Case study · 2026
A catalogue for a physical video game collection that runs on the desktop and on the phone against the same data. I built it because switching apps meant losing the collection on the way out.
Switching apps cost the whole collection
Anyone who catalogues a physical collection knows the problem: you pick an app, enter hundreds of items, and a year later you need to leave it. The export comes out incomplete: photos gone, condition gone, whatever was specific to your collection gone. The next app’s import does not understand half the fields. What is left is retyping.
I looked for an alternative, paid ones included. What I found solved one platform at a time: either a decent phone app with no desktop counterpart, or a desktop catalogue that does not come along when you are checking a shelf in a shop. Bulk entry wants a keyboard; looking something up wants the phone in your pocket. Neither one on its own does the job.
So I built it. One database, three ways to reach it, and data entry that pulls as much as it can from public catalogues instead of demanding typing.
Three attempts, two discarded
The project did not start as a web app. The final choice came from eliminating whatever could not hold the promise of working at both ends.
The gain is not technical, it is maintenance: a fix ships to the site and reaches the phone in the same moment, with no store release involved.
Cataloguing 500 items without typing 500 times
A mid sized collection makes the manual form unworkable. Every game has a title, platform, genre, publisher, year, cover, condition and region. Multiplied by a few hundred, that is weeks of work. The app has to fill in whatever it can find out on its own.
Search by name, with multi select
Type Mortal Kombat and IGDB returns the dozens of versions in the franchise. A filter panel shows the platforms found with a count for each; you tick PlayStation 2, the list shrinks to the PS2 releases, and you tick everything you own in one pass.
Barcode through the camera
Point at the box and the app opens the game’s record. A read is only accepted after three identical frames, because a single bad frame had already opened the wrong game once.
Console catalogue imported
All 131 IGDB consoles and handhelds on a single screen, with logos, grouped by family. Tick the units you own and they all come in with name, manufacturer and image already filled.
Two copies of the same game are not the same row
Here is why generic catalogues do not work for a physical collection. They treat Kingdom Hearts as one record with a counter. But the Japanese and the American release are different items: different box, different manual, different price. A plain PSP and a special edition PSP are not two units of one item: they are two items.
The model here treats every copy as its own record, with its own address. Region is part of the item’s identity:
Region stored as NTSC-U, PAL,
NTSC-J, which are television signal standards.
It forced a mental translation on every entry.
Place names. The migration converted what already existed
and regenerated the addresses:
/ps2-kingdom-hearts-japao.
Consoles got the same logic, plus one extra field. On a PlayStation 4 Pro Kingdom Hearts, Pro is the hardware revision and Kingdom Hearts is the edition. Those are two distinct facts, and a single field would let them drift apart over time. Each unit also carries a serial number, the accessories that came with it, and whether it still works.
When you own two PSPs, each record links to the other unit, and an other unit button opens the form with the identity copied and the per unit fields blank, because copying a serial number and a condition would produce a wrong record by default.
What only shows up once you call the API for real
Documentation does not warn you. These came from probing the APIs and watching what actually came back.
IGDB does not index retail barcodes
The initial assumption was that external_games
would carry the UPC from the box. Inspecting a well known
game, there were only digital store identifiers: an Amazon
ASIN, a Microsoft Store ID, a Steam ID, a YouTube channel.
No physical code at all.
The way out was a two step chain: UPCitemdb returns the product name, a cleaner strips the brand and edition noise, and the clean title goes to IGDB. With a ladder of fallbacks: the full term, then the part before the colon, then without the numeric blocks, then the first three words. When confidence is low, the app refuses to open a record and hands the user back to manual search.
The category field vanished from /platforms
The query filtering by category returned
HTTP 200 with zero results, no error, as if
IGDB had no consoles at all. The type is now
platform_type, a reference to a new table. A
filter on a field that no longer exists fails silently, which
is the most expensive failure mode to diagnose.
IGDB search ranks the exact match poorly
Searching for Returnal put the game in eighth place. And the search chokes on punctuation: a title with a colon and a hyphen returned zero results, while the same text with spaces in place of those marks put the game first.
The fix was to request three times as many results and reorder them by similarity to the typed term, normalising the punctuation before sending.
Android’s WebView darkens inherited colours
In the APK, text showed up almost black on a dark background. Three wrong hypotheses later the pattern became visible: only inherited colours were affected; explicitly declared ones survived. Declaring the colour on the text elements fixed it.
The filter saved the wrong platform, silently
Filtering by PlayStation 2, ticking everything and adding saved the games as Xbox. The filter hid the results without PS2 but did not change the platform selected on each card, which stayed the first one IGDB returned, in arbitrary order.
An error that does not announce itself is worse than one that breaks: you would only find out months later, looking at a shelf that does not match the catalogue. Filtering by a platform now selects that platform; when two filters apply to the same item, the choice is left to the user instead of being guessed.
The public showcase is generated, not served
Showing the collection to other people should not open the catalogue to the internet. One command generates a static, self contained HTML file: no PHP, no database, no session, no form. There is nothing to inject and no login to force.
What comes out is an explicit allowlist of fields, never the whole object. That way a new column in the table never becomes an accidental exposure. These are always left out:
- Serial number. It is what proves ownership on an insurance claim or a police report. Made public, it lets someone else claim the unit is theirs.
- Price. A public list of rare items with values next to them is an appraised inventory.
- Own photos. A file from a phone usually carries EXIF with the coordinates of where it was taken.
The most concrete threat to a collection showcase is not a server breach. It is the collection itself becoming a target.
In production, in use
It runs on shared hosting with Git deploys, DNS on Cloudflare and SSL. The APK points at the same address, so shipping a fix updates both ends at once.
Next steps
- Ship to the stores. Today the APK is installed by hand. The next step is taking it to Google Play and, with the same Capacitor that wraps Android, building the iOS version for the App Store. The codebase is already shared; the work is in each store’s requirements.
- Automatic pricing. Today PriceCharting is reached through a shortcut that opens a pre filled search; the next step is bringing the quote into the record itself.
- eBay. Cross referencing sold listings to estimate real market value, which tends to differ from the guide price.
- Importing from other catalogues. Closing the loop and solving, for other people, exactly the problem that started the project.