Skip to content

Good Fights

A fight-rating app for combat-sports fans — live on the App Store and Google Play, with a catalogue going back to 1993.

Role
Founder · sole developer
Platforms
iOS · Android · Web
Stack
React Native (Expo), Node/Express in TypeScript, PostgreSQL + Prisma, Next.js, Docker on a Linux VPS, Cloudflare R2
Good Fights home screen: upcoming cards grouped by day, each with its poster, start time, broadcaster and a list of hype-rated fights.The Upcoming tab: promotion filters, an event hero, Canadian broadcast listings for Sportsnet+ and Paramount+, and the main card with a hype score against each fight.Rating a fight: Swanson vs Choi at UFC 206 scored 10 out of 10 on a colour-graded scale, with a written review and a link to the comments.

The problem

Film fans have Rotten Tomatoes. Fight fans had nothing — no place to rate an individual fight, see what the crowd thought, or find the great ones they missed.

Fighting Tomatoes, my earlier web version, proved people wanted this. Good Fights is the mobile-first successor, built to live on both app stores with a real back-end behind it.

What shipped

  • iOS and Android apps from one React Native codebase, published through App Store and Play Store review.
  • Accounts with email and Google sign-in, and a migration that carried the existing Fighting Tomatoes user base onto the new platform — 3,415 registered users who have submitted 76,516 fight ratings and 1,450 written reviews.
  • A catalogue of 15,730 fights across 1,635 events dating to 1993 — 10,567 fighters, results and stats — kept current by daily scrapers.
  • A REST API in TypeScript on PostgreSQL, shared by the apps and the Next.js web version at goodfights.app.
  • Claude-based enrichment that writes fight and fighter detail, gated on a confidence score so nothing the model is unsure about is published — with unit tests that check its output, including one that verifies quoted material is real.
  • 34 scheduled GitHub Actions workflows running the scrapers, enrichment, database backups, deduplication and content-freshness checks.
  • A Remotion video pipeline that renders promo clips from live database data, with generated voice-over, plus 3,670 automatically written news articles.
  • Over-the-air updates so fixes reach users without a new store submission.

How it fits together

Mobile app and web front-ends talk to a REST API backed by PostgreSQL; daily scrapers feed the database.Mobile appReact Native · ExpoWebNext.js · goodfights.appREST APINode · Express · TSDatabasePostgreSQL · PrismaDaily scrapersVPS cron · results & stats

The hard parts

Two app stores, one release

Getting through Apple and Google review on every version, and keeping a release cadence that doesn't leave users stranded on an old build.

Updating without resubmitting

Over-the-air updates have rules about which builds they can target. Getting that right meant fewer store submissions and faster fixes.

Trusting a model in production

An LLM that is right most of the time is not good enough when the output is published as fact. Every enriched field carries a confidence score and is dropped below a threshold, and the enrichment code is unit-tested like any other service — including a test that checks quoted material against the source.

Running the whole stack myself

Docker and nginx on a Linux VPS, Prisma migrations against Postgres, Cloudflare R2 for images, cron for the overnight jobs, PostHog for behaviour. No platform team — scheduled jobs moved off paid CI onto a server I run, with enough monitoring to know when something breaks.

Outcome

76,516
fight ratings submitted by users
3,415
registered users
15,730
fights catalogued across 1,635 events

Have something like this in mind?

Start a project