Skip to content
Moku
@moku-labs/room pages

How it works

A star of WebRTC channels: phones send intents, the stage owns the state, signaling only opens the door.

On this page

The picture

Star topology
 phone ── intents ──►  ┌───────┐  ◄── intents ── phone
                       │ stage │
 phone ◄─ snapshot ──  │ state │  ── snapshot ─► phone
          + deltas     └───────┘     + deltas

One device is the host: the stage. Up to eight phones connect to it. Phones never connect to each other. The stage holds the only writable copy of the state. A phone holds a replica and changes the state only by asking.

  1. Create. stage.createRoom() mints the code and returns at once. The signaling join runs in the background.
  2. Rendezvous. Stage and phone meet on the signaling adapter under the room code, and exchange SDP and ICE.
  3. Connect. A direct WebRTC DataChannel opens. Public signaling is left once the channel is up.
  4. Baseline. The stage sends the phone a full snapshot. room:sync-ready fires on the phone.
  5. Play. The phone sends intents. The stage validates them, changes state, and broadcasts deltas up to 30 times a second.

Two planes

PlaneCarriesAPI
The WireIntents, snapshots, deltas, heartbeats, the roster. Everything per-frame.onIntent, intent, mutate, read, on
Moku eventsCoarse lifecycle: six room:* events. No gameplay payload.hooks in your plugin

Events are local to the device that emits them. They do not cross the network.

EventPayloadFires onWhen
room:peer-joined{ peerId }stageA phone's channel opened and it entered the roster.
room:peer-left{ peerId }stageThe heartbeat declared a phone dead.
room:host-reconnecting{}stageThe page loaded and found a saved room.
room:sync-ready{}bothStage: the first registerSlice. Phone: the first applied frame.
room:intent-undeliverable{ name, cSeq }phoneAn intent ran out of retransmits.
room:network-warning{ reason }bothice-failed, rendezvous-unreachable, channel-closed or room-evicted.

Seven plugins

PluginMounted atWiringJob
transportapp.transportdefaultWebRTC, the Wire, signaling, heartbeat.
sessionapp.sessiondefaultCode, QR, roster, reload record.
intentapp.intentdefaultPhone-to-stage input, validation, delivery.
syncapp.syncdefaultStage-to-phone state.
stageapp.stageyou add itThe host's short API.
controllerapp.controlleryou add itThe phone's short API.
hubapp.hubworker appSelf-hosted signaling on Cloudflare.

The facades add no behavior. They shorten the engines' API and re-declare the six events, so a plugin with depends: [stagePlugin] gets typed hooks for all of them. The facades do not expose registerSlice or register. Call those on app.sync and app.intent.

What the signaling adapter changes

AdapterRendezvousAfter connectUse
publicRendezvous()Public Nostr relays through TrysteroLeftThe default. No infrastructure.
serverSignaling(url)One WebSocket to your hubStays openOwn infrastructure, eviction notice, room reclaim, TURN credentials.
inMemory()One processLeftTests. No RTCPeerConnection.

ICE servers

iceServers defaults to "auto".

ValueResult
"auto" with serverSignalingFetches TURN credentials from the hub's /api/ice. Any failure falls back to public STUN.
"auto" with other adaptersOne public STUN server.
[]No servers. Host candidates only: same network.
An arrayYour servers, for example your own TURN.
An async functionCalled on each connect, for short-lived credentials. undefined, a throw or a timeout falls back to public STUN.