Case Study
VGDB

Overview
VGDB is a video game database with a community built on top of it, and it is my second attempt at the idea. The first, We The Players, taught me what the shape should be; this is the ground up rebuild. It catalogues around 200 platforms across 50 years, 190,000 games and 119,000 companies, then layers on ratings, rich text reviews, ranked collections, screenshots, follows and comment threads. There is also a curated YouTuber and website directory, an aggregated news hub, and a small browser arcade. I designed and built all of it myself, web first, with an iOS app in progress.
The Problem
We The Players was my first attempt: a community site for rating and reviewing games, built as a React and Vite single page app on Supabase. The social mechanics worked. The thing underneath them did not. A rating site is only as good as the catalogue it rates, and mine was thin, so most pages were empty or a stub. A client rendered single page app is also close to the worst possible shape for a product whose value is a few hundred thousand indexable pages.
The wider itch was older than the code. Every time I looked something up about an old console I ended up in three Wikipedia tabs and a dead citation. The archives with real depth felt lifeless, and the sites with life in them barely catalogued anything. I wanted both in one place: a proper archive with editorial weight, and a clubhouse on top for people who want to log what they play. So VGDB is not a refactor. It keeps the idea and throws the implementation away.
Constraints
- One person. Product thinking, design, schema, code, data pipelines and copy, with nobody to hand a ticket to.
- Time. First commit 30 March 2026, launch target 22 September 2026. Under six months.
- Money. Bootstrapped, one Supabase project, one small host. That budget shaped real decisions, including running staging against the production database.
- Data. Nobody hand types 190,000 games. The catalogue had to be assembled from licensed, credited sources: IGDB, RAWG, Steam, Wikidata, NexusMods and the YouTube Data API. Each came with a quota ceiling and its own terms. YouTube requires stored channel data to be refreshed or deleted inside 30 days, so a daily refresh job was a prerequisite, not a nicety.
- Moderation. Screenshots and comments mean reporting, blocking and suspension, and I am the only moderator.
- Law. UK GDPR and PECR, so consent and erasure had to be built rather than promised.
- Scale. Hundreds of thousands of pages means chunked sitemaps, keyset pagination and per role query timeouts.
Approach
1. Catalogue first. Seed the platforms from a CSV, bulk import games from IGDB, then backfill companies, external IDs and a Wikidata crosswalk. Nothing social got built until there was something worth being social about.
2. Set the house style early. Dark teal, sharp corners, one token set and a small kit of primitives, documented, so I stopped reinventing buttons at midnight.
3. Get the security model right before the user data existed. Row level security on every table, column level grants on anything sensitive, then a full audit in April and a hardening pass against its findings.
4. Build the social layer on top: ratings, reviews, collections, follows, comments, notifications, blocks.
5. Add discovery and editorial surfaces: browse and filter, compare, timeline, genres, companies, on this day, news and curated resources.
6. Build the admin CMS so I was never editing production data by hand.
7. Wire the unglamorous parts: transactional email, consent, analytics, error monitoring, subscription tiers.
8. Treat the iOS app as a sibling rather than a monorepo, and give it a thin REST surface wrapping the same server actions the web already used.
Solution
- A catalogue of roughly 200 platforms across 50 years, 190,000 games and 119,000 companies, with franchises, series, hierarchical genres, modes and age ratings.
- Platform pages with full spec sheets, history, media, trivia and long form editorial stories, plus side by side compare, a decade timeline and a generations browser.
- Game pages with a cinematic hero, six tabs, community rating aggregates, screenshots, trailers, external links and NexusMods data where a match exists.
- A community layer: 10 point ratings, rich text reviews with their own indexable URLs, ranked and unranked collections with drag reorder and per item notes, screenshot uploads with spoiler blur, follows, likes, comment threads on four entity types, notifications, blocking and reporting.
- Public profiles with seven sub-pages, and a separate owner dashboard covering library, reviews, collections and settings.
- A curated YouTuber directory with search, content type, platform and language filters, five sort orders and a nightly refresh against the YouTube API, plus a companion Websites directory.
- A news hub that aggregates the RSS already collected for the Websites directory into top stories and content type sections, adding no new infrastructure.
- A browser arcade with two playable games, Higher or Lower and Guess the Year, both judged server side so the answers never reach the client.
- Editorial articles, a public roadmap with community voting, a hand curated changelog and an ethics page carrying a review integrity pledge.
- A full admin CMS for every entity, plus moderation queues, a duplicate game merge tool and IGDB and Steam import surfaces.
- Accounts with email, Apple and Google sign in, a seven step onboarding wizard, referral invites, Steam library linking, eight transactional email templates, cookie consent, subscription tiers and Stripe checkout.
- A sibling React Native app of 54 screens, served by 59 dedicated endpoints over the same action layer.
Outcomes
This build proved the architecture rather than the market. At the last measurement before launch the site had 138 registered players, 215 public reviews and 42 public collections. Those are small numbers, and it had not been publicly announced, so I will not dress them up. What it did establish is more useful to me:
- 56 named releases in under six months, every one documented on a public changelog.
- 169 migrations and 71 tables under full row level security, with zero database advisor errors at the last audit.
- 3,127 passing unit tests across 434 files, alongside Playwright smoke and accessibility specs.
- Zero
any, zero@ts-expect-errorand zero non-null assertions in production code. - One server action layer serving both a web app and a native app, which is the thing I most wanted to prove.
- Hundreds of thousands of game pages made genuinely indexable through chunked sitemaps and per page metadata, which the old single page app could never have managed.
Reflections
- Make the tests gate something on day one. I had 3,127 tests and none of them could block a merge. Worse, CI was permanently red because of one unpatchable advisory, so red stopped carrying any information at all. Branch protection is ten minutes of work that I kept deferring.
- Never share a database between staging and production. It saved a few pounds a month and created the single largest structural risk in the system: no safe migration rehearsal, staging builds contending with live traffic, and a restore runbook that would have restored over production.
- Accessibility belongs in the linter, not in an audit. Only six accessibility rules were enabled and all as warnings. Alt text came out perfect because it was linted; hundreds of form controls carried no error association because it was not.
- Design privacy at the same time as the feature. View tracking minted and stored a session identifier before it consulted consent, and the policy page shipped with visible placeholders in it.
- Ship less scaffolding. I built a complete billing pipeline that has never processed a single event. That could have waited for demand.
- Consolidate the paperwork earlier. I accumulated two dozen competing tracking documents before moving everything to one issue tracker. Documentation is worth writing; a second, third and fourth backlog is not.
- Measure the cold path, not the warm one. Query timeouts and sitemap concurrency caught me out three separate times, and each time the cache hid the problem until a cold build ran.