Using Matchmaking

Open a room and share a code

POST /matchmaking/games/duel/rooms

A session is minted with the host already in it, and a code is returned. This is one of four ways in.

POST /matchmaking/rooms/WXYZ-1234/join

Join tolerates lowercase, whitespace and a missing dash. The code’s alphabet already excludes I, O, 0 and 1.

Invite somebody directly

POST /matchmaking/games/duel/invites
{ "email": "ada@example.com" }

Or a screen name — best-effort, because auth exposes no unique one. A name matching zero users or several is refused rather than guessed.

Route
GET /matchmaking/invitesPending, for the caller
POST /matchmaking/invites/:id/acceptMints the session and seats both
POST /matchmaking/invites/:id/decline

Exactly two players. A game with a larger roster is refused here and told to use a code or the queue.

Friends

The fourth way in is the waiting queue, which needs provisioning before it will pair anybody.

Route
GET /matchmaking/friends
POST /matchmaking/friends/:userId/request
POST /matchmaking/friends/:userId/acceptMutual accept forms the edge
POST /matchmaking/friends/:userId/decline
DELETE /matchmaking/friends/:userId

The routes mount only when friends is on — that flag, and every other, is in the reference.

Queue

POST   /matchmaking/games/duel/queue
GET    /matchmaking/games/duel/queue
DELETE /matchmaking/games/duel/queue

A player learns they matched through presence, not by polling.

Presence

GET /matchmaking/presence      (WebSocket)

On connect: pending invites, and which friends are online now.

Then three events — match_found, invite, friend_request.

The identity is a server-set header from the authenticated handler, stashed so it survives eviction. The object never trusts a client-supplied user id.

Nothing here is a hard dependency

Each peer is reached through a seam, and its absence degrades one thing rather than breaking the composition:

AbsentWhat happens
AuthEvery route denies. The right failure — a pairing layer with no identity has nothing to pair
RatingThe queue buckets by region only. No skill matching, everything else unchanged
MultiplayerEvery pairing path still runs, and none of them can mint the session it exists to produce

Install them and it lights up, with no wiring.

Mount it elsewhere

basePath moves the whole surface. The friend routes follow it.

What it stores

pithy_matchmaking_invitesDirect invites, indexed by invitee, carrying the session id once accepted
pithy_matchmaking_friendsOne row per pair, indexed both ways
KV, matchmaking:<code>Room codes

Room codes belong in KV rather than D1 because a room code is a short-lived pointer with a TTL and a use counter — which is exactly what KV is, and nothing else here is.

Queue state lives in the queue object’s own storage, and presence is the live socket set. Neither is a row you keep.

ESC