Open a room and share a code
POST /matchmaking/games/duel/roomsA 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/joinJoin 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/invites | Pending, for the caller |
POST /matchmaking/invites/:id/accept | Mints 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/accept | Mutual 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/queueA 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:
| Absent | What happens |
|---|---|
| Auth | Every route denies. The right failure — a pairing layer with no identity has nothing to pair |
| Rating | The queue buckets by region only. No skill matching, everything else unchanged |
| Multiplayer | Every 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_invites | Direct invites, indexed by invitee, carrying the session id once accepted |
pithy_matchmaking_friends | One 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.