hyhy.gg

Case study

Dizzy.gg

A rankings and tournament platform for competitive gaming communities, with Glicko-2 ratings, live brackets and start.gg and Challonge imports. It runs in production across 3 games and 10 communities.

Live at dizzy.gg

games
3
games
communities
~10
communities
players ranked
5,590
players ranked
tournaments
796
tournaments
sets recorded
30,028
sets recorded

in its first three months, built and run by one person

The problem

Competitive gaming communities track skill in spreadsheets and run brackets across three separate tools. The hard part is identity. The same player shows up under different tags on different platforms over years of events, so nobody can say who the best player is right now. Dizzy.gg gives every player one profile with moderated identity claims, calculates a rating from their full match history, and runs the tournaments that create that history.

It works for any game and any community. Games are stored as database rows, and each community has its own rating pool and moderators, so adding a new game is a config change with no migration. I've kept to that since the first schema and it still holds at 63 models and 118 migrations. That's why three games and ten communities run on it today.

My role

Just me. I came up with the idea, designed and built it, and I maintain and run every part of it.

Capture pending

The dashboard community moderators use to run their scene.

What's technically interesting

A pure Glicko-2 rating engine over 30,000 sets

Ratings come from a deterministic module with no side effects. It processes matches in date order, applies decay for inactive players, and ranks by a conservative estimate (rating − k·deviation) so a volatile newcomer can't jump above a proven player. Because it's deterministic, I can safely recalculate all 30,028 sets against the live leaderboard. The same engine runs a separate rating pool for each community, with rules for when ratings go stale and a script for full recalculation.

The live leaderboard, ranked by conservative Glicko-2 ratings, with the game switcher open on all three games.
The live leaderboard, ranked by conservative Glicko-2 ratings, with the game switcher open on all three games.

Architecture rules enforced by the build

The build checks packages/domain and packages/contract for purity and fails on any impure import, so nobody can quietly break the rule. Typechecking runs twice, once with tsc and once with tsgo.

End-to-end type safety across three clients

About 62 oRPC procedures across 15 routers, with contracts defined in Zod. The Next.js web app and an Expo React Native app share the same domain and contract packages, and a public REST API with generated OpenAPI docs is built on the same definitions. Change a contract and every client picks it up at compile time.

Capture pending

The Expo app on a phone, the third client of the same typed contract.

Importing other people's messy data

Brackets from start.gg and Challonge come in shapes that don't normalise cleanly, like single-stage double elimination, grand-final resets, and pools that feed into a bracket. Fixing these after the fact also meant writing migration scripts for old data and recalculating ratings across the whole site.

A live tournament bracket, one of 796 imported or run on the platform.
A live tournament bracket, one of 796 imported or run on the platform.

Safe migrations and fast tests

CI checks for migration drift against a clean Postgres 18 container, and production deploys take a pg_dump backup before migrating. Tests run in parallel, with each CPU worker getting its own copy of a seeded template database and a guard that refuses to touch any database not named for testing.

Capture pending

The OBS stream overlay at its native 1920×1080, laid over gameplay.

Stack

  • TypeScript
  • Bun workspaces monorepo
  • Next.js 16.2 (App Router, RSC)
  • React 19.2
  • Tailwind v4
  • Base UI + shadcn
  • Expo 57 / React Native 0.86
  • oRPC + Zod
  • PostgreSQL 18
  • Prisma 7
  • Better Auth (Discord OAuth)
  • Pusher
  • Stripe
  • Vercel

Proof

  • 63 database models · 118 migrations · ~29 route pages
  • Over 100 architecture decision records
  • ~286K lines of source across 1,246 files; ~107K lines of tests across 400 test files