Skip to main content
Back to articles
2026 / 01
| 3 min read

Identity, SEO, and a Lot of Polish

Identity got real: guest sessions, passkeys, and a server‑backed auth surface, plus gopher content routing, Z‑Machine version wiring, and a formatting sweep.

emulator authentication passkeys seo z-machine
On this page

Guests now have real sessions, which meant wiring identity from the browser through the modem layer to the backend and then deciding what happens when a guest upgrades to a registered account mid-session.

Two IDs, One Path

Identity splits into two layers:

  • A browser UUID for cloud storage and device continuity
  • A session handle for the BBS experience

The UUID is generated once and stored in localStorage. It’s boring and stable, which is what equipment sync needs. The handle is the human face: new visitors get a GUEST_XXXX handle and a local session, and if they register, that handle becomes a real account and the session upgrades in place.

The upgrade path is explicit in code. A guest session becomes a registered session without losing local achievements, unlockables, or equipment; on upgrade I merge local data with server data and let the server win for equipment, since that reflects the last device used. A guest is a valid state, and “upgrade” is a data merge rather than a restart.

Passkeys Fit That Model

A passkey is a credential that upgrades a guest into a real account, so it slots into the same path rather than running beside it.

The UI only shows passkeys when the browser supports WebAuthn and the server reports passkeys_enabled. On the server, passkeys are enabled only when the database is available, because WebAuthn needs the ceremony state to survive between the begin and finish calls. If there’s no database, passkeys don’t exist.

Wiring that through the UI was the rough edge. The server status flag has to propagate all the way to the button render, so if the backend is offline the button doesn’t appear at all. Slightly awkward to plumb, but better than a button that leads to a cryptic error.

SEO: Making the Surface Honest

I gave the site the metadata it was already implying:

  • Per‑page title and description
  • Open Graph + Twitter Card tags
  • JSON‑LD for the main site and manuals
  • Canonical URLs where they matter

None of this changes the product. It changes what a crawler can tell about the product, which until now was very little.

Gopher: Content From the Server

The gopher backend now pulls content from /api/gopher/map and /api/gopher/file, which lets the server own the filesystem and keeps the client stateless. The server reads from GOPHER_CONTENT_PATH, defaulting to ./server/gopher-content, and the backend simply requests selectors. The gopher client stays a terminal; the server becomes the source of truth.

Z‑Machine: V5 and V8

Version handling is now explicit. V5 gets its own handler set, and V8 reuses those handlers to unlock the larger address space. V6 and V7 still have placeholder implementations, so the supported set is V1–V5 and V8.

The Formatting Pass

I ran formatting across the whole stack:

  • Biome for TypeScript
  • clang‑format for C
  • ruff for Python

Every diff after this one is smaller.

Next

Identity and metadata are in place, so I can start moving the emulator from hardcoded backends to server‑backed services without reinventing user state each time. The gopher migration is the pattern; the bulletin board and file browser are next.

Previous: 2026-01-30