Add Matchmaking

pithy add matchmaking

No required prerequisites, and three optional ones that each change what you get. Add auth at minimum — every route denies without it.

What lands in your repo

apps/<worker>/pithy.config.ts gains the registration, with an example game you are meant to replace:

matchmaking({
  games: [
    {
      key: "duel",
      snapshot: { kind: "connect-n", rules: { rows: 3, cols: 3, connect: 3 } },
    },
  ],
  friends: true,
  basePath: "/matchmaking",
}),

apps/<worker>/wrangler.jsonc gains four bindings, in every environment stanza:

BindingTypeWhat it is
DBd1Rooms, invites and the friend graph
MATCHMAKINGkvShort-lived room codes
QUEUEdurable_objectThe open queue that pairs waiting players
PRESENCEdurable_objectThe WebSocket that pushes match-found, invite-received and friend-request

And your Worker’s entry module gains two exports:

export { MatchmakingQueue } from "@pithy-sh/matchmaking/src/queue/durableObject";
export { MatchmakingPresence } from "@pithy-sh/matchmaking/src/presence/durableObject";

A Durable Object binding names a class, and wrangler resolves that name against the module your main names. Without the export, deploy is refused. add writes both, plus both class migration tags, across every environment stanza — which is the wiring that makes this a capability rather than a snippet.

One migration runs, creating the room, invite and friendship tables.

Declaring a game

Each entry is a game players are matched into, and its snapshot declares the multiplayer session a match mints.

games: [
  {
    key: "duel",
    snapshot: { kind: "connect-n", rules: { rows: 6, cols: 7, connect: 4 } },
  },
],

The key lines up with the multiplayer game of the same name, which is what makes composing the two need no wiring. With multiplayer absent, session minting is off and the four ways in still work — they just do not produce a game at the end.

Turning the friend graph off

friends: false,

That unmounts request, accept, decline and remove entirely. Turn it off for a game where players only ever meet through a code or a queue — an unmounted route is better than a mounted one nobody uses, and there is nothing to moderate that does not exist.

What composing the optional three turns on

ComposeEffect
authRoutes stop denying. Sessions, invites and friendships bind to a real player
ratingThe queue buckets by skill as well as region, widening each player’s band the longer they wait
multiplayerA match mints a real session. Without it, matchmaking pairs players and hands back nothing to play

None is required, and each absence has a defined behavior rather than a crash.

Before production: the sweep cadence

The queue’s Durable Object wakes on an alarm to pair waiting players, and that cadence is the one number here that can surprise you on a bill.

A presence socket is cheap — Cloudflare’s Hibernation API means a connection waiting on nothing bills no duration. A queue that wakes every second to check an empty lobby is not.

What a waiting queue costs has the arithmetic at several queue sizes and how to tune it. Read it before you ship rather than after.

Check it worked

pithy doctor reports matchmaking under the Worker’s health, and the bindings line checks both halves of both Durable Objects — the config entries and the two exports.

In dev, create a room code with one seeded user and redeem it with another. pithy dev runs both Durable Objects locally through Miniflare, so the whole path is exercisable with no account.

ESC