Auto guest sign-in
Goal
Give every visitor a session automatically — no login form, no password.
Pattern
A guest is a real account the browser creates for itself: a generated email and a generated password, both stored locally. There is no login form, and the account is a normal one — the player can add a magic link or a social identity to it later and keep their progress.
This used to be one call to a dev bypass. That bypass is gone (see the note
below), and the replacement is barely longer, because a brand-new address is
exactly the case where register returns a session immediately.
const stored = JSON.parse(localStorage.getItem('guest') ?? 'null');
const guest = stored ?? {
email: `guest-${crypto.randomUUID().slice(0, 8)}@demo.local`,
// Generated, kept locally, never shown. It is what lets this browser get back
// into the SAME account tomorrow -- lose it and the guest is a new player.
password: `Aa1!${crypto.randomUUID()}`,
};
await client.auth.register(guest);
localStorage.setItem('guest', JSON.stringify(guest));
On subsequent visits, restore the stored session first and only sign in again if it
has lapsed. Note this is login, not register: the address exists now, and
register would refuse it.
await client.session.restore();
if (!client.session.getToken()) {
await client.auth.login(guest);
}
Store the guest credentials separately from the bearer token.
BrowserLocalStorageTokenStore holds whichever token is current — the identity
session token right after sign-in, then the app-scoped token once you mint
it (below).
A magic link needs the player to open an inbox, which is exactly the friction a
guest flow exists to avoid. Password is the automatic path. If you would rather
not hold a password in localStorage, requestLoginLink is the alternative and
costs you the inbox round trip.
The dev bypass this chapter used to recommend is gone — devLogin was deleted
on 2026-08-20, along with the devToken shortcut, because both handed out a
session with no proof that the caller owned the address. Nothing replaces them on
any tier. See Sign in.
Mint an app-scoped token for gameplay
Sign-in returns an identity session token: a management-plane credential that the
Game API and UDP surface reject. While you hold it (right after signing in), mint a
short-lived app-scoped token for AppId=1 — that token is what chapters 3+ use:
// Direct (same-origin) mint — the session token authorizes the call.
const appToken = await client.portal.mintAppToken('1');
client.setToken(appToken.token); // gameplay now uses the app-scoped token
mintAppToken returns { token, gameTokenId, appId, expiresAt, gameApiUrl, gameApiWsUrl, launchUrl }. App tokens are short-lived (~30 min): call client.portal.refresh() before expiresAt to keep playing the same app. On a returning visit where restore() loaded a still-valid app token you can skip straight to chapter 3; if it expired, refresh() or sign in again and re-mint. When the game runs at a different origin from your identity/Overworld page, use the PKCE portal flow (client.portal.beginEntry / handleAuthorizeRequest / completeEntry) instead — AppId=1 (the Overworld) is trusted, so it skips the consent screen; untrusted apps prompt the player first. See Portals & app-scoped tokens and Game API authentication.
Reset
Provide a "Reset guest" control that clears the stored guest credentials and calls
client.auth.logout(). Clearing them is what makes the next visit a new player;
the old account still exists and is simply unreachable from this browser.
Exit criteria
- Refresh preserves session
- Incognito window gets a new guest account
Next: Connect & bootstrap