Multiplayer
Available since v0.48.0
maypop.multiplayer connects the people who have an app open at the same
time. Traffic goes directly between their devices, end-to-end encrypted;
Maypop only introduces them. Nothing is stored: when the last person
leaves, the session is gone. Anything that must outlast the session belongs
in the KV store.
There are two building blocks:
| You want | Use |
|---|---|
| who’s here, whose turn it is, a shared board at human speed | join() — a room |
| cursors, live positions, game ticks, chat-like messages | connect() — a message session |
interface MaypopMultiplayer { join( roomId?: string, opts?: { presenceTimeoutMs?: number }, ): Promise<MaypopRoom>; connect(roomId?: string): Promise<MaypopP2pSession>;}Room ids are 1–64 characters of letters, digits, _, . and -; the
default is "main". Use different ids for different tables, matches or
documents.
Rooms: shared state and presence
Section titled “Rooms: shared state and presence”interface MaypopRoomPeer { id: string; username: string | null; avatarUrl: string | null; state: unknown; updatedAt: string; self: boolean;}
interface MaypopRoom { readonly id: string; readonly me: string; readonly peers: MaypopRoomPeer[]; readonly state: unknown; setState(value: unknown): void; setMyState(value: unknown): void; subscribe(cb: (room: MaypopRoom) => void): () => void; leave(): Promise<void>;}A room has one shared value (state, last write wins) and one value per
person (setMyState, which never conflicts). Rapid writes are merged, so
rooms suit changes at the pace people make them.
await maypop.ready();const room = await maypop.multiplayer.join("table-1");
room.subscribe((r) => { renderPlayers(r.peers.map((p) => ({ name: p.username, ready: p.state }))); renderBoard(r.state);});
room.setMyState({ ready: true });- Someone who joins late receives the current state from the people already there.
- A peer drops out of
peersafterpresenceTimeoutMswithout a heartbeat (default and maximum 45 seconds). - Read-only viewers join as spectators: they see the room but aren’t in
peers, and their writes fail withmaypop/read-only.
Message sessions: live traffic
Section titled “Message sessions: live traffic”interface MaypopP2pPeer { id: string | null; endpointId: string; username: string | null; avatarUrl: string | null; self: boolean; connected: boolean;}
interface MaypopP2pSession { readonly id: string; readonly me: string; readonly peers: MaypopP2pPeer[]; send(endpointId: string, payload: unknown): Promise<void>; broadcast(payload: unknown): Promise<number>; onMessage(cb: (msg: { from: string; payload: unknown }) => void): () => void; subscribe(cb: (session: MaypopP2pSession) => void): () => void; leave(): Promise<void>;}const session = await maypop.multiplayer.connect("canvas");
session.onMessage(({ from, payload }) => moveCursor(from, payload));window.onpointermove = (e) => session.broadcast({ x: e.clientX, y: e.clientY });- Payloads are JSON, up to about 1 MB.
- Delivery is best effort and unordered, with no history: a message sent while someone is away never reaches them.
sendto a peer that isn’t connected fails withmaypop/not-connected.- Peers find each other within a few seconds. Reloading the page gives a
person a new
endpointId.
The multiplayer engine downloads the first time an app calls it, so join when the person starts a session rather than on page load.