Forty games and not one sign-up screen

Play Pixel Games started as a question - how much of a game can you ship if the player is never allowed to create an account, download anything, or wait?

Vue 3VitePrerenderedZzFXParseMongo

The Play Pixel Games arcade gallery - a violet CRT-styled list of games with a day-streak counter.

The rule I set on day one was that the game had to be playable within one second of the page loading, with one thumb, and without me knowing anything about the person playing. No login, no download, no install. Everything after that was a consequence of that rule.

It sounds like a marketing line. In practice it's a brutal engineering constraint, and it deleted about half the architecture I would otherwise have reached for.

No account means no server, mostly

The obvious casualty is the backend. If there's no account, there's nothing to load before the first frame - no session check, no profile fetch, no "restoring your progress" spinner. So the whole front end is a static build: Vite and Vue 3, compiled to a folder of HTML, CSS and JS I can drop on any host.

Routing was the one place I refused the easy path. Hash routes would have been simpler, but /play/tilt-maze is a URL a person can read and a search engine can index, and /#/play/tilt-maze is not. So it's HTML5 history routing, and every route is pre-rendered to a genuine static HTML file at build time. Forty-odd games, forty-odd real pages. The build also sets base: './' so it works in a subfolder as well as at a domain root - which mattered more than I expected.

The daily challenge, without a database

Every game has a Wordle-style daily challenge: everybody gets the same puzzle on the same calendar day. The instinctive way to build that is a server that generates today's puzzle and hands it out. But that's an account system wearing a hat - now I need a backend on the critical path, and my one-second rule is dead.

Instead the puzzle is a pure function of the date. The calendar day seeds a deterministic pseudo-random generator, and the generator produces the level. Two players on opposite sides of the world get identical puzzles because they computed the same thing from the same seed, not because they asked the same server.

const seed  = hash(todayISO() + gameSlug)
const level = generate(seed)   // same input, same level, everywhere
2026-07-18 the calendar day hash seed deterministic Player in Pune generate(seed) → level 1042 Player in Lisbon generate(seed) → level 1042 Player in Toronto generate(seed) → level 1042
No request leaves the device. Everyone computes the same level from the same date, so the “shared” daily puzzle needs no server to share it.

Streaks live in localStorage. This is a real trade-off and I want to be honest about it: clear your browser data and your streak is gone. I decided a lost streak was a smaller cost than an account wall, and I'd make the same call again - but it is a cost, and it's the single thing people write to me about.

The arcade gallery on a phone - the layout everything was designed for first.

Sound with no audio files

Forty games with sound effects is normally a pile of megabytes and a loading screen. It ships with zero audio files instead: ZzFX synthesises every blip, thud and jingle procedurally at runtime from a handful of numbers. The entire sound design weighs less than one .mp3 would.

The fonts got the same treatment - Press Start 2P and Baloo 2, self-hosted as woff2, no Google Fonts request. Partly performance, mostly that I didn't want a third party logging every visitor to a site whose whole promise is that nobody's watching.

A game running: Tilt Maze, one of the forty.

The night I filled my production database with junk

There is a backend, just not on the critical path - leaderboards and a lightweight identity live on Parse and Mongo behind api.playpixelgames.com. And here is the mistake I made, written down so I don't repeat it.

The front end's API base defaulted to production. Not localhost - production. So the moment I started the dev server on its own and clicked around, my test scores went straight into the live database. The leaderboard writes weren't gated on hostname either, so nothing stopped it. I spent an evening cleaning rows out of a production collection that I had put there myself, one careless npm run dev at a time.

A default that points at production isn't a default. It's a loaded gun with the safety off.

The fix was unglamorous: never run the front end without a local backend beside it, point the dev build at a same-origin proxy, and write the rule at the top of the repo's guide in language my future self can't misread. Config that's dangerous when forgotten should fail loudly rather than default quietly.

What I'd do differently

  • Make the environment explicit from commit one. No implicit production. Refuse to boot without a target.
  • Design the streak-loss story earlier. An optional one-tap export would have cost a day and saved most of the complaints.
  • Prune sooner. Forty games is a lot of surface for one person. The best eight carry the site.

None of that changes the founding constraint, though. Open the page, and you're playing. That part still works, and it's still the reason the thing exists.