Identity, SEO, and a Lot of Polish
Building out the identity system—sessions, OAuth, passkeys—plus gopher content flow, Z-Machine expansion, and a huge formatting sweep.
Identity, SEO, and a lot of polish
This was the longest day of the project so far. I built an entire identity system, ran a full SEO sweep, and did the kind of formatting cleanup that makes a codebase feel professional instead of hacked together.
Identity: retro feel, modern auth
The identity system has a weird challenge: I want the convenience of modern authentication without losing the “dial into a BBS, pick a handle” experience. The solution was a three-tier model:
- Anonymous UUID — You get a unique ID the moment you load the page
- Guest — Pick a handle, start saving preferences
- Registered — Full account with OAuth or passkeys
The first-visit flow is particularly satisfying. You see a period-appropriate “WELCOME NEW USER” screen, press any key, and you’re into the system. Later you can upgrade to a real account if you want persistence.
(I spent way too long debating whether to call it “login” or “sign on.” The 80s kid in me won: it’s “sign on.”)
Passkeys: biometric auth for a fake BBS
The passkey implementation was technically unnecessary and I love it anyway. WebAuthn with biometric unlock—all so you can sign into a system that pretends to be from 1985.
// FIDO2 WebAuthn registration
const credential = await navigator.credentials.create({
publicKey: {
challenge: challenge,
rp: { name: "Emulator.ca", id: "emulator.ca" },
user: { id: userId, name: handle, displayName: handle },
pubKeyCredParams: [
{ type: "public-key", alg: -7 }, // ES256
{ type: "public-key", alg: -257 }, // RS256
],
authenticatorSelection: {
authenticatorAttachment: "platform",
userVerification: "preferred"
}
}
});
The server stores the credential ID and public key. Next time you sign on, you authenticate with your fingerprint or face. The contrast between cutting-edge auth and retro UI is exactly the vibe I wanted.
(For more on the FIDO2 implementation, see Deep Dive: Passkey Authentication.)
The great SEO sweep
I updated all 35+ HTML pages with proper meta tags. Not because I expect huge search traffic, but because doing it right costs almost nothing and makes the site legitimate:
- Title tags with site name suffix
- Meta descriptions for each page
- Open Graph and Twitter Card tags
- JSON-LD structured data (TechArticle, SoftwareApplication, Organization)
- Proper favicon variants (SVG, ICO, Apple Touch Icon)
- Sitemap and robots.txt
The manual pages were the bulk of the work—each one needed unique meta data. I updated the Markdown-to-HTML converter to generate everything automatically.
Z-Machine: V5 and V8 support
The Z-Machine got serious attention too. V5 games are the sweet spot (most Infocom titles), and V8 is basically V5 with a larger address space. Both now work:
// V8 handlers reuse V5 implementation
case 8:
return handle_v5_opcode(zm, opcode);
I also added V4 scroll region support for split windows and initialized the interpreter header fields properly. Some games check these during startup and behave strangely if they’re zero.
The formatting sweep
The last piece was pure maintenance: applying Biome, clang-format, and ruff across the entire codebase. This sounds boring but it’s important:
style: apply biome formatting to web/src/backend
style: apply biome formatting to web/src/lib
style: apply biome formatting to web/src/main, video, tui, workers
style: apply biome formatting to web/src/serial
style: apply clang-format to cores C code
style: apply clang-format to languages C code
style: apply ruff formatting to llm_game_player scripts
Consistent formatting means diffs are about logic, not whitespace. It means new contributors don’t have to learn our idiosyncratic style. It means the codebase can grow without accumulating formatting debt.
I also did a Canadian English pass on the manuals—colour instead of color, honour instead of honor, organise instead of organize. Because if you’re going to pretend to be a Canadian project, you might as well spell like one.
Performance and build improvements
Some quick wins that compound:
- Lazy-load backends: Main bundle dropped from 1,057KB to 104KB (90% reduction!). Backends load on-demand while the dial tones play.
- CSS optimization: Added preload hints, eliminated render-blocking imports, enabled bfcache for documentation pages.
- Dockerfile caching: Added Cargo cache mounts for 2-5x faster rebuilds.
The lazy loading is particularly nice. When you dial a number, the handshake sounds play while the backend code downloads in parallel. By the time you hear “CONNECT 2400”, the backend is ready.
What changed
- Built complete identity system: sessions, first-visit onboarding, OAuth, passkeys, profiles
- Added SEO infrastructure to 35+ pages
- Expanded Z-Machine to V5/V8 with proper header initialization
- Ran formatting pass (Biome, clang-format, ruff) across entire codebase
- Added Canadian English to all manuals
- Optimized build with lazy-loading and caching
What I was going for
A proper “account” layer without losing the retro feel. A site that presents well to search engines and screen readers. A codebase that doesn’t embarrass me when I look at diffs.
What went sideways
The first-visit modal had a bug where it wouldn’t detect Enter key when xterm had focus. ViewportScaler initialization order caused a blank screen until the user pressed a key. OAuth needed route syntax corrections after the sweep. The usual cleanup cascade.
What’s next
The platform felt like a real product. Next was migrating to a server-backed architecture—microservices, proper databases, and peripherals that feel physical.
Previous: 2026-01-30