Pool Matchmaking API
Pools are named matchmaking queues. Clients enter a pool, the server pairs them into groups atomically, and each matched client receives a server-generated session name via a matched event. Matched clients are not auto-joined — they call join() with the returned session name themselves. Matchmaking precedes session membership: you do not join a session before entering a pool.
Pools are scoped to the connection's Project — matchmaking pairs members only within the same Project. There are two ways to enter (see Entering a Pool): the ad-hoc form, where the client supplies the pool config, and the policy form, where the client names a declared matchmaking policy and the server owns the config.
For conceptual background and end-to-end flows, see the Pool Matchmaking workflow, the Pools section of Core Concepts, and Matchmaking as a Policy.
Pools use WebSocket only — pool frames are never sent over RTC.
Pool Modes
The mode is fixed when the pool is created and is immutable for the pool's lifetime.
| Mode | Who matches | Members visible | Relevant methods |
|---|---|---|---|
auto | Server (FIFO, respects filter) | No | enter, matched$ |
claim | Any member (first claim wins) | Yes | enter, members$, claim, matched$ |
mutual | Both members (each must claim the other) | Yes | enter, members$, claim, claimRejected$, matched$ |
propose | One proposes, the other accepts/rejects | Yes | enter, proposal$, accept, reject, matched$ |
delegated | A matchmaker-role client via assign() | Matchmaker only | enter (role: matchmaker), members$, assign, matched$ |
Entering a Pool
enter has two forms:
- Ad-hoc —
enter(name, { groupSize, mode?, … }). The client names the pool and supplies its config. Available in implicit and non-lockdown declared Projects; rejected withproject.lockdownin a locked-down Project. - Policy —
enter({ policy }). The client references a declared matchmaking policy by name (or omitspolicyto use the Project's default). The policy ownsmode,groupSize, andfilter, so the client cannot set them.
Policy resolution (policy form):
- Omitting
policyuses the Project'sdefaultpolicy. If the Project declares no default, the server returnspool.policy_required. - Naming a policy that doesn't exist returns
pool.policy_not_found. - Policies are only meaningful in declared Projects; an implicit Project has none, so only the ad-hoc form works there.
For auto policies the SDK also offers client.joinProject(project, { policy }), which connects, enters the policy, awaits the match, and joins the session in one call — see Matchmaking as a Policy.
TypeScript API — Pool class
Access the pool interface through client.pool (a Pool instance). Defined in sdks/typescript/src/pool.ts; types in sdks/typescript/src/pool-types.ts.
| Member | Signature | Description |
|---|---|---|
enter (ad-hoc) | enter(poolName: string, options: PoolEnterOptions): Promise<PoolEnterResult> | Enter a client-named pool with client-supplied config. In claim-based modes, seeds members$ with the current member list. |
enter (policy) | enter(options: PoolPolicyEnterOptions): Promise<PoolEnterResult> | Enter by declared policy name (or the Project's default). The server owns mode/groupSize/filter. |
leave | leave(poolName: string): void | Leave the pool and clear local state (fire-and-forget). |
claim | claim(poolName: string, targetId: string): void | Claim a specific member (claim and mutual modes). |
accept | accept(poolName: string, fromId: string): void | Accept a proposal (propose mode). |
reject | reject(poolName: string, fromId: string): void | Reject a proposal (propose mode). |
assign | assign(poolName: string, groups: string[][]): Promise<StarfishFrame> | Matchmaker only — assign groups (delegated mode). Resolves with the assign response frame. |
members$ | Observable<PoolMember[]> | Live member list, updated by member-joined / member-left events. |
matched$ | EventStream<PoolMatchedEvent> | Fires when the server matches you; carries pool, session, and peers. |
proposal$ | EventStream<{ pool: string; from: string; attributes?: Record<string, unknown> }> | Fires when a peer proposes a match (propose mode). |
claimRejected$ | EventStream<{ pool: string; target: string }> | Fires when a claim you made is rejected (mutual mode). |
Python API — StarfishClient pool methods
The Python SDK exposes pool methods directly on StarfishClient. Defined in sdks/python/starfish/client.py, backed by sdks/python/starfish/pool.py.
| Method | Signature | Description |
|---|---|---|
pool_enter | async pool_enter(options: PoolEnterOptions | PoolPolicyEnterOptions) -> PoolEnteredResult | Enter the pool. Pass PoolEnterOptions for the ad-hoc form or PoolPolicyEnterOptions for the policy form. Raises RuntimeError on a pool.not_found error. |
pool_leave | async pool_leave(pool: str) -> None | Leave the pool (fire-and-forget). |
pool_claim | async pool_claim(pool: str, target: str) -> None | Claim a specific member (claim and mutual modes). |
pool_accept | async pool_accept(pool: str, from_: str) -> None | Accept a proposal (propose mode). |
pool_reject | async pool_reject(pool: str, from_: str) -> None | Reject a proposal (propose mode). |
pool_assign | async pool_assign(pool: str, groups: list[list[str]]) -> StarfishFrame | Matchmaker only — assign groups (delegated mode). |
pool_members | pool_members(pool: str) -> Observable[list[PoolMember]] | Per-pool observable, updated by member-joined / member-left events. |
pool_matched | pool_matched -> EventStream[PoolMatchResult] (property) | Fires when the server matches you. |
The Python SDK surfaces the
matchedand member-list events. Proposal and claim-rejected events (proposeandmutualmodes) are received by the server-side handlers but are not currently exposed as Python observables.
Types
TypeScript
type PoolMode = "auto" | "claim" | "mutual" | "propose" | "delegated";
type PoolRole = "member" | "matchmaker";
// Ad-hoc form: the client supplies the pool config.
interface PoolEnterOptions {
groupSize: number;
mode?: PoolMode; // default: "auto"
role?: PoolRole; // default: "member"
meta?: Record<string, unknown>; // member metadata (wire field: `attributes`)
filter?: Record<string, string>;
create?: boolean; // omitted → not created; send true to create
}
// Policy form: the client references a declared policy; the Project owns the config.
interface PoolPolicyEnterOptions {
policy?: string; // omit to use the Project's default policy
role?: PoolRole; // default: "member"
meta?: Record<string, unknown>;
}
interface PoolMember {
id: string;
meta?: Record<string, unknown>; // wire field: `attributes`
}
interface PoolEnterResult {
pool: string;
members: PoolMember[];
}
interface PoolMatchedEvent {
pool: string;
session: string;
peers: PoolMember[];
}enter() resolves with a PoolEnterResult (the resolved pool name and its current roster). assign() resolves with the raw StarfishFrame response; read response.payload for the matched groups and their session names.
Python
# Ad-hoc form: the client supplies the pool config.
@dataclass
class PoolEnterOptions:
pool: str
create: bool | None = None # omitted → not created; send True to create
mode: str = "auto" # "auto" | "claim" | "mutual" | "propose" | "delegated"
group_size: int = 2
role: str | None = None # "member" | "matchmaker"
attributes: dict | None = None
filter: dict | None = None
# Policy form: the client references a declared policy; the Project owns the config.
@dataclass
class PoolPolicyEnterOptions:
policy: str | None = None # omit to use the Project's default policy
role: str | None = None
attributes: dict | None = None
@dataclass
class PoolMember:
id: str
attributes: dict = field(default_factory=dict)
@dataclass
class PoolMatchResult:
pool: str
session: str
peers: list[PoolMember]
@dataclass
class PoolEnteredResult:
pool: str
mode: str
group_size: int
members: list[PoolMember] = field(default_factory=list)
createis opt-in in both SDKs. The pool is created only when you passcreate: true(TypeScript) /create=True(Python). An omittedcreateis join-only: entering a pool that doesn't exist returnspool.not_found. The policy form has nocreatefield — declared policies are provisioned out-of-band.
metavsattributes. The TypeScript SDK exposes member metadata asmeta(serialized asattributeson the wire); the Python SDK names itattributesthroughout.
Protocol Message Types
Wire-level frames, all with resource: "pool". Request/response pairs share a method and are distinguished by kind (request vs response); events use kind: "event".
| Method | Kind | Direction | Description |
|---|---|---|---|
enter | request | client → server | Enter a pool |
enter | response | server → client | Acknowledgement; includes the current member list in claim-based modes |
leave | request | client → server | Leave a pool (no response) |
claim | request | client → server | Claim a specific member (claim/mutual modes) |
claim | response | server → client | Pending acknowledgement (mutual mode, before the match completes) |
accept | request | client → server | Accept a proposal (propose mode) |
reject | request | client → server | Reject a proposal (propose mode) |
assign | request | client → server | Matchmaker assigns groups (delegated mode) |
assign | response | server → client | Confirmation with the matched groups and their session names |
matched | event | server → client | Match fired; carries the session name and peers list |
member-joined | event | server → client | A member entered the pool (visible members / matchmaker only) |
member-left | event | server → client | A member left (carries memberId and reason) |
proposal | event | server → client | A peer proposed a match (propose mode) |
claim-rejected | event | server → client | A claim was rejected (mutual mode) |
member-left reasons: "left", "matched", "timeout", "disconnected".
enter payload fields
| Field | Type | Required | Description |
|---|---|---|---|
policy | string | no | Name of a declared matchmaking policy (policy form). When present, pool/create/mode/groupSize/filter are supplied by the policy and MUST NOT be set. Omit to use the Project's default policy. |
pool | string | ad-hoc only | Pool name. Ad-hoc form only; omit when using policy. |
groupSize | number | ad-hoc only | Clients per match group (used only on creation) |
create | boolean | no | Create the pool if it does not exist (ad-hoc form) |
mode | string | no | Matchmaking mode (default "auto"; used only on creation) |
role | string | no | "member" (default) or "matchmaker" (delegated mode) |
attributes | object | no | Opaque member metadata; available to filters and visible members. (The TypeScript SDK exposes this as meta.) |
filter | object | no | Attribute constraints for auto mode; literal values or "@self" |
Error Codes
| Code | Raised by | Description |
|---|---|---|
pool.not_found | enter (ad-hoc) | The pool does not exist and create was false or omitted. Python raises RuntimeError. |
pool.mode_mismatch | enter, claim, assign | Operation not allowed in this pool's mode (e.g. role: "matchmaker" outside delegated mode, or claim in auto mode) |
pool.policy_not_found | enter (policy) | The referenced matchmaking policy is not declared on the connection's Project. See Troubleshooting. |
pool.policy_required | enter (policy) | No policy was named and the Project declares no default policy. See Troubleshooting. |
project.lockdown | enter (ad-hoc) | Ad-hoc creation attempted in a locked-down Project. See Troubleshooting. |
Entering a pool no longer requires joining a session first — matchmaking precedes session membership.