A browser game that saves progress, no account
· 5 min
Most browser games handle progress one of two ways. Either it lives in
localStorage and vanishes the first time someone clears their browser, or you
are asked to make an account before you have decided you like the thing.
There is a third option: a browser game can save your progress without an account at all. Give every visitor a real server-side player row, with no signup, and let signing in be an upgrade rather than a gate. Astrototo does this. The interesting parts are the one endpoint that refuses anonymous players, and the compiler error that enforces it.
How a browser game saves progress without an account
The first time you open the game — /play, not the landing page — the browser
mints a random UUID, keeps it in localStorage under andromeda:anon, and
exchanges it for a session token. The server creates a player row.
An anonymous player is not a special case. It is an ordinary row with a null
email and a null google_sub. There is no parallel "anonymous save" table and
no branch through the save code. Two tables would have meant two code paths, and
the anonymous one would rot.
Sessions last 180 days, because being logged out mid-voyage is hostile in a game you are meant to play in two-minute pieces.
What a server-side save actually buys
Progress survives a reload, a new tab and a closed browser.
Be precise about the mechanism, because it is the opposite of what you might
assume: localStorage is still the synchronous write, and the server is a
debounced mirror. The push is fire-and-forget on a 1200 ms timer with the
error swallowed. A failed push loses nothing the player can see, and a crash
inside that window loses the last change server-side.
The server copy is what lets a voyage outlive the page, and what lets us see the
game at all. Before it existed the whole run lived in one localStorage key —
invisible to us, so we had no idea whether anyone finished an episode or where
they stopped.
It also means nothing ever demands an account. There is a sign-in button on the home page and on the Chart, blocking nothing. Exactly one thing requires an identity, and it is the Pay button.
When progress without an account is lost
Clear your site data and the run is gone. The id lives in the browser; when
it goes, the server holds a row it can no longer connect to anyone. Private
browsing is worse — if localStorage throws, the code falls back and you get a
new anonymous player on every load.
This is the trade we mentioned in passing in what a browser game gives up by having no download; here it is the whole post.
We chose it. The alternative is device fingerprinting, and the argument against it is mechanical rather than moral: fingerprints collide often enough to occasionally hand one person another person's save, and the implementations accurate enough to be worth using are a paid third-party service. Signing in fixes it honestly, and the cost of declining is stated rather than hidden.
What happens when an anonymous player signs in
Two cases, wanting opposite behaviour.
First sign-in. The anonymous row gains an email and a Google subject id. It stops being anonymous and becomes the account. Nothing is copied — the save and the run history come along because they never moved. A migration that shuffles records between rows is a migration that can fail halfway, and you find out from someone whose progress vanished.
Signing in to an account that already exists. Now there are two players: the anonymous one from this browser and the real one from before. We leave the anonymous row exactly where it is and adopt the account's own save. No merge, no "keep the further one", and no prompt — asking is a question about consequences the person cannot see yet, at the moment they were trying to do something else.
The server tells the client which of the two happened and returns the
authoritative save either way, so the client never guesses. Accounts are keyed
on google_sub, never email: an email can change hands, a subject cannot.
A headless harness walks both cases across two simulated browsers — 14 checks, including the abandoned anonymous row surviving a sign-in on a second device.
Why anonymous players cannot buy anything
Checkout returns 403 SIGN_IN_REQUIRED without a google_sub.
This is not a rule the anonymous model invented. The signup gate and the money gate were already the same door; what changed is that playing stopped being behind it.
The part worth stealing is how it is enforced. Because player.email is
nullable, the payment-gateway call — which needs a real customer email — does
not compile until the anonymous case is handled. The type error is the feature.
The obvious shortcut, a placeholder email for anonymous players, would make the
column lie and the guard disappear.
You can start a voyage without telling us anything. It will remember where you got to.