mirofish0x

Shipping 12 browser games in one codebase

16 Jul 2026 · by Rahul

Neon Arcade has twelve games: snake, a flappy-style one-tap game, a 2048 clone, a reflex timing game, breakout, a falling-blocks stacker, a bubble shooter, minesweeper, tic-tac-toe against an AI, dots & boxes, a Simon-style memory game, and a doodle-jump climber. They all live in one Next.js app, share one account system, and each got built in a matter of hours, not days.

That speed didn't come from typing fast. It came from one architectural decision I made after game number two, when I could feel the copy-paste pain coming: games are data, not features.

The registry

There is a single file, src/lib/games.ts, that is the only source of truth for what games exist. One entry per game:

{
  slug: "snake",
  name: "Snake",
  tagline: "classic, progressive speed",
  maxPlausibleScore: 300,
  minMsPerPoint: 900,
}

Everything reads from this. The hub page maps over it to render the game grid. The leaderboard API checks slugs against it. Even the anti-cheat limits live here — each game declares its own maximum plausible score and minimum time-per-point, so the server can reject impossible submissions without any game-specific code paths.

Adding a game means exactly three things: one registry entry, one game component, one thin page file that mounts it at /<slug>. Nothing else changes. No new API routes, no new auth wiring, no leaderboard work. By game eight I had the routine down to: build the fun part, fill in two anti-cheat numbers, push.

The shell does the boring 80%

The other half of the trick is a shared GamePage component that wraps every game. It owns the login/signup panel, fetches the run token before play starts, submits the score after, renders the per-game daily leaderboard, and fires the little celebration when you crack the top three.

A game component receives a tiny props contract — essentially "here's a callback for when the run starts, here's one for when it ends with a score" — and everything inside is its own private world. Snake does its grid ticks, the memory game does WebAudio tones, breakout does its collision maths. None of them know that accounts or Redis exist.

This boundary is what kept twelve games from becoming twelve maintenance problems. When I fixed a leaderboard bug, I fixed it once.

Two honest confessions

I renamed Tetris. My falling-blocks game is called "Stax" because "Tetris" is a trademark and I'd rather pick a different name than get a takedown letter over a free game. If you're cloning a classic, check the name before you ship it — mechanics generally aren't protectable, but names are.

I skipped pinball. It was on my original list of thirteen. But good flipper physics is genuinely hard — the difference between real pinball feel and a mushy ball is months of tuning, and a bad pinball game would drag the whole arcade down. Shipping a bubble shooter instead was the right call. Knowing which game not to build turned out to be as useful as the registry pattern.

Would I do it this way again?

Yes, with one change: I'd write the registry on day one instead of day two. The first game always feels too small to deserve architecture. It isn't. The second game is already too late.


Play all twelve at playneonarcade.vercel.app — and if you beat my Stax score, I don't want to hear about it.