Sem categoria

Jogo Velho Collector

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.

Where it started

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.

My Games screen: a grid of covers with 75 games and a filter bar for console, series, condition and region
75 games in the collection. The badges do the heavy lifting: the Crayon Shin-chan is a Japanese Super Nintendo copy, loose in the box; the Ecco the Dolphin is Mega Drive. Three facts that decide price and rarity, readable without opening anything.
How it got here

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.

01
Native app in Flutter One binary for Android, another for Windows. The camera on Windows never worked and the app froze on frame sync. Dropped before it turned into debt.
02
Local portal on XAMPP It solved bulk entry on the desktop but died at the front door: no access from outside, no phone. It served as a prototype of the data model.
03
Laravel deployed + PWA + APK One application, one database, one address. The browser serves the desktop, the PWA installs itself, and Capacitor wraps that same address into an APK with a native back button. No syncing, because there are no two copies to sync.

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.

The core problem

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.

IGDB search for Kingdom Hearts, with platform chips showing the number of results on each one
A search for Kingdom Hearts returns the whole franchise. The chips count how many results exist per platform: 6 on PS2, 5 on PS5, 1 on GBA. Ticking one narrows the list to what is on your shelf.

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.

Import screen: IGDB consoles grouped by family, each with a logo and a checkbox
The hardware catalogue grouped by family, with each logo. Ticking and adding in bulk avoids typing the name, manufacturer and generation of 131 consoles one at a time.
131 Consoles in the catalogue
3 Ways to add an item
46 Commits to production
1 Database behind every screen
What off the shelf apps get wrong

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:

Americas Europe Brazil Other
Before

Region stored as NTSC-U, PAL, NTSC-J, which are television signal standards. It forced a mental translation on every entry.

After

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.

Engineering findings

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.

Integration

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.

Integration

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.

Integration

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.

Platform

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.

Fix

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.

Publishing without exposure

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.

Where it stands

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.

Laravel 12 PHP 8.2 MySQL Blade Bootstrap 5.3 Capacitor 6 Service Worker BarcodeDetector API IGDB UPCitemdb

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.

A personal project, built and maintained entirely by me. Written in August 2026.