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. That sounds small, but it meant wiring identity from the browser all the way through the modem layer to the backend, 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 gets generated once and stored in localStorage; it’s boring and stable, which is what I want for equipment sync. The handle is the human face. New visitors get a GUEST_XXXX handle and a local session. 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 can become 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 (last device used). That’s the smallest behaviour: a guest is a valid state, and “upgrade” is a data merge, not a restart.

Passkeys Fit That Model

Passkeys are not a parallel auth system; they are a credential that upgrades a guest into a real account.

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 requires persisted ceremony state. That constraint eliminated a lot of ambiguity: if there’s no database, passkeys don’t exist.

Wiring this through the UI turned out to be the rough edge. The server status flag has to propagate all the way to the button render. If the backend is offline, the button doesn’t appear—the honest UI is the more painful one, but it’s the only one that doesn’t lie.

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, but it changes what the product claims to be. The browser sees a BBS; search engines should see a coherent site with a schema, not a pile of HTML.

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.

Small architectural win: 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

Not glamorous, but it makes every diff smaller and every future refactor cheaper.

Next

Identity and metadata are in place. Tomorrow I can start moving the emulator from hardcoded backends to server‑backed services without reinventing user state again. The gopher migration proved the pattern works; now I need to apply it to the bulletin board and file browser.

Previous: 2026-01-30