Build record / one-file constraint

How I built an offline idle game in one HTML file

The playable part of Oh God It Grows is one HTML document: interface, CSS, game state, procedural creature art and the complete simulation. That constraint made the game easy to ship. It also forced every system to share a room.

By Kieran Simpson · published 21 August 2026 · approximately 11 minutes

A precise claim: the gameplay itself is one HTML file. The public site also has ordinary information pages, images, a web-app manifest, a service worker, one small anonymous game-event endpoint and an optional Plausible page counter. The counter is not a gameplay dependency. “One file” describes the application, not every byte on the domain.

I wanted the first interaction to be nearly frictionless. There is no install screen, character creator or account form. A specimen is waiting in a jar and accepts a tap. Fifteen biomass later it can grow an organ, then production starts to automate itself. The entire technical design grew around protecting that immediacy.

A framework would have been perfectly capable of building it. I chose not to use one because the game did not need server-rendered components, a dependency graph or a client router. It needed a state object, a reliable clock and a great many sentences about inadvisable anatomy.

The whole loop is state plus time

The central state object is called S. It holds biomass, organs, adaptations, mutation picks, permanent traits, catalogues, cosmetics, achievements and the timestamps required for saving and offline progress. The simulation itself is deliberately boring. A short interval calculates elapsed real time, adds organ and reflex income, checks thresholds, updates the visible readout and periodically saves.

elapsed time │ ├── organs per second ├── autonomous reflex feeds └── temporary multipliers │ ▼ gain biomass │ ┌──────┴──────┐ ▼ ▼ redraw state check mutation │ ▼ choose a card

The loop uses measured elapsed time rather than assuming that every interval fires on schedule. Background tabs are throttled aggressively, especially on phones. If the simulation awarded exactly one tenth of a second on every callback, leaving the tab in the background would quietly slow the game down.

Most calculations are derived from state rather than stored as additional state. Total organ output, tap value, reflex income, mutation thresholds and the scientific name can all be recomputed. This makes migrations safer because old saves contain canonical facts, not every result the interface once displayed.

The creature is SVG assembled at runtime

There are 42 normal mutation choices and 12 secret hybrid forms. Drawing a complete sprite for every possible accumulated combination was not realistic, so the renderer converts mutation IDs into procedural SVG features. Body shape, hue, appendages, eyes, surface marks, late-stage effects and regalia are composed into one string and inserted into the jar.

The same renderer draws a living specimen and the preserved creatures in the Forbidden Garden. A garden entry stores the lineage and a small amount of display state, not a frozen picture. That avoids having two renderers drift apart and keeps 24 saved specimens reasonably small.

Procedural art solves asset count and creates genuinely personal lineages, but composition is the hard part. Early versions added every new detail forever. A terminal creature became a noticeboard of wings, spikes, rings, text and tiny symbols. The first repair separated anatomy from surface marks, removed text from the animal and capped low-priority marks. The next renderer pass is more structural: at major eras inherited traits will be translated into new body plans instead of remaining identical stickers.

SVG also made share cards possible without a graphics dependency. The game composes a larger specimen card, turns it into a data URL, draws it to a canvas and offers a PNG through the native share sheet when the browser supports files.

Local saves, with migrations and a manual escape hatch

The normal save lives in browser local storage under a stable domain-level key. Moving the game from the homepage to /play does not lose progress because local storage belongs to the origin, not the URL path.

Every saved object has a version. Loading validates the broad structure, applies sequential migrations, rebuilds display strings from the game’s own tables and sanitises fields that can affect rendering. A malformed save is quarantined rather than silently overwritten. Imported names and labels are never trusted as markup.

Local storage is convenient but it is not a backup. Browsers can clear it, private windows discard it and mobile storage policies can be severe. The game asks for persistent storage once a player has made progress, then reports the result honestly. It also exports a portable save code.

Those codes began as base64-encoded JSON. They became unwieldy once catalogues and garden specimens grew, so current browsers use the native CompressionStream API to gzip the save before encoding it. The decoder still accepts the old format. Both encoded and decompressed sizes have strict limits so an imported code cannot ask the page to allocate unbounded memory.

Offline play has two separate meanings

Idle games often say “offline progress” when they mean that the next visit awards production for time spent away. Oh God It Grows does that at half rate for up to eight hours. Closing the game and leaving its tab hidden use the same calculation: the gap is capped, the current offline multiplier is applied and the result is granted once. A hidden tab therefore cannot quietly bypass the offline economy at full speed.

It also works without a network connection after the site has been loaded. A root-scoped service worker caches the game and small shell. HTML requests are network-first so online players receive a patched game instead of remaining stuck on an old cached document. Static assets are cache-first. If the network fails, the most recent successful copy opens.

The service worker is deliberately optional. The game still works if registration fails. The Records panel tells the player whether offline launch is ready because discovering the truth after losing a connection is not helpful.

A strict content policy around an inline game

Putting the complete program in an inline script makes a strict Content Security Policy less convenient. Allowing all inline JavaScript would remove much of the policy’s value. Instead, deployment headers allow only the exact SHA-256 hashes of the known inline scripts. Inline event attributes are disabled, framing is denied, external fonts and media are blocked, and network connections are restricted to the same origin.

This means every real game edit changes a script hash and must update the policy. That is annoying in precisely the useful way: a modified inline program cannot execute unless the release process explicitly recognises it.

Save import is treated as hostile input even though saves usually come from the same player. Canonical IDs are looked up in internal tables, arrays and numbers are bounded, unknown cosmetics are ignored, and saved human-readable text is rebuilt rather than injected. A shareable code is an input format, not a trusted database row.

Anonymous measurement without a player identifier

I still need to know whether anyone reaches the first organ or extinction. The game can send a small set of coarse events to a same-origin Cloudflare endpoint. An event describes what happened, a broad age bucket for the browser’s first visit, visit count and a few progression totals. It contains no cookie, random user ID, fingerprint or stored IP address. The switch in Records disables it immediately.

This design cannot answer every marketing question and it should not pretend to. It can show a funnel from session to first tap, organ, mutation and extinction. It cannot follow one named person around the site, which is the point.

The bugs that changed the architecture

A correct-looking number can destroy the campaign

A prestige trait once carried a percentage of the previous life’s biomass into the next. The percentage seemed modest. Because later lives reached astronomical totals, it eventually started new specimens beyond most of the mutation tree. The endgame appeared stale because the player was skipping it. Carryover is now capped to an early mutation threshold.

Visual accumulation is not evolution

Giving every card a unique mark stopped exact duplicates but did not make late creatures coherent. The audit showed that silhouette, posture, material and camera need to evolve, not merely the feature list. A renderer can be technically procedural and still produce visually repetitive results.

Responsive order must exist before it is visible

On phones, the three desktop columns become one flex sequence. Saved progress also reveals and reorders long-term panels. Applying that state after the first paint produced a severe layout shift even though the final layout was correct. The page now completes save hydration, panel visibility and ordering while the game surface is hidden, then reveals the settled interface. Returning layouts are also explicitly applied after the asynchronous load.

Single-file does not mean untested

The release checks extract the inline game script and ask Node to parse it, verify required security headers and hashes, confirm cache and route assumptions, and assert several balance and accessibility invariants. Syntax is only the first layer. The important failures in this kind of application are often valid JavaScript executed in the wrong order or state.

What the one-file constraint bought me

The costs are real. A 200 KB document is not naturally modular, CSP hashes require release discipline, and drawing plus simulation plus interface code can become tightly coupled. The answer is not pretending the file is small. It is creating internal seams: canonical tables, pure calculations, explicit migrations, one renderer and automated checks around the behaviours that must not change.

I would not use this architecture for a multiplayer game, an account dashboard or anything with large media assets. For a private, deterministic browser organism with an alarming amount of paperwork, it remains a good fit.

The source story makes more sense once the thing is moving.
The game is free and starts without an account.

Open the one-file game