Identity & authentication
Firebase ID tokens, verified in-worker
Every ordinary /api/engine/* request carries a Firebase ID token as
Authorization: Bearer <token>. A WebSocket first obtains a signed, 60-second,
game-scoped ticket over that authenticated HTTPS channel and presents the ticket
on the upgrade; the Firebase credential never enters a URL. The Worker verifies
Firebase tokens with jose against Google's
securetoken JWKS: RS256 pinned (no algorithm confusion), issuer and audience
checked against the configured FIREBASE_PROJECT_ID, expiry enforced. A failure
is a deliberately unspecific 401: signature, expiry, issuer, and audience
failures all read the same to a client ("re-authenticate").
Socket tickets are HS256 JWTs with fixed issuer and audience plus the game id, user id, expiry, and unique id. The upgrade verifies the signature, scope, and expiry before it derives the game's Durable Object.
The verified claims carry the uid, isAnonymous (the anonymous
sign-in-provider claim, which drives every guest gate), and the profile fields
(Google supplies name + picture, Apple usually only email, guests none).
Provisioning & guests
A users row appears on first sight of a valid token, so there is no signup
call to make. Username is derived from the email local part (sanitized to a
[a-z0-9_.]{3,20} charset) or a generated player_NNNNN handle for guests, with
a collision-retry loop.
Guests are first-class: anonymous sign-in gives a real uid and a real (ephemeral)
account. Because linkWithCredential preserves the uid, guest→permanent
conversion is an in-place backfill on the same row, and the provider's display name
and avatar overwrite the guest's, while the stable username handle survives.
Guest capability is deliberately narrowed: guests may play (including vs bots,
unrated) but cannot create friends-access games or join rated games. Inactive
guests are swept by the cron; see Account lifecycle.
The username is the stable, editable handle (distinct from the provider
display name, which the engine never lets a user edit). PUT /me/username
validates the same [a-z0-9_.]{3,20} charset and returns a clean 409 on a
collision (the column is UNIQUE). The display name and avatar come from
the auth provider (or an uploaded avatar); PUT /me/avatar is the only way a
user changes their picture.
The social graph
Friendships, search, and blocking are cross-game and D1-only; they never
touch a Durable Object. The relationships table stores one row per unordered
pair in canonical order (user_id_1 < user_id_2) with a status
(pending / accepted / blocked) and an initiated_by actor, so a single
shared row encodes the relationship and the direction of a request or block is
recovered from initiated_by.
- Requests.
POST /friends/requests {targetUserId}inserts apendingrow, unless the target already has a pending request out to the caller, in which case it auto-accepts (sending back is accepting).accepttransitions the request the other party initiated.DELETE /friends/{id}is the single idempotent unfriend / withdraw / decline. All writes require a registered caller, and a friend target must be registered too (a guest is a throwaway identity). - Blocking.
POST /friends/{id}/blockoverwrites any pending/accepted row (or creates one) asblocked, recording the blocker ininitiated_by. A block in either direction refuses new requests; only the blocker canunblock. - Search.
GET /users/search?q=is a case-insensitive substring match on username or display name, excluding the caller, guests, and anyone in a blocked relationship with the caller, ranked exact → prefix → substring.LIKEtoday, D1 FTS5 later; the%wildcard is stripped so a caller can't force a scan. - Discovery.
GET /friends/gameslists joinable games created by the caller's accepted friends, the lobby that makesfriends-access games reachable.
Friend-event pushes (friend_request, friend_accepted) fire from the route
through the shared, required FCM path. Because these
run in a stateless Worker (not the always-alive DO), they ride
executionCtx.waitUntil so a slow FCM call never delays the response, the one
place the engine uses waitUntil deliberately.