Multiplayer API (BETA)
Server-authoritative real-time multiplayer rooms. Game logic runs on the server in a GameRoom class; clients connect via ServerRoom to exchange messages.
Overview
The multiplayer system has two parts:
Server — You write a
GameRoomsubclass that holds all game state and validates every action. The server is the single source of truth.Client — Players connect through
ServerRoomand send typed messages.
// Client — join by matchmaking or room code
import RundotGameAPI from '@series-inc/rundot-game-sdk/api'
const room = await RundotGameAPI.realtime.joinOrCreateRoom<MyProtocol>('tictactoe')
const room = await RundotGameAPI.realtime.joinRoomByCode<MyProtocol>('HX9KWR')
// Server
import { GameRoom } from '@series-inc/rundot-game-sdk/mp-server'
export default class TicTacToe extends GameRoom<MyProtocol> { ... }Setup
Vite plugin
Add rundotMultiplayerPlugin to your vite.config.ts. This builds your server room code, copies rooms.config.json to dist/, and starts a local dev server for testing:
configPath
string
'rooms.config.json'
Path to your rooms config file
devPort
number
3001
Port for the local dev server
threaded
boolean
false
Run dev rooms in Worker Threads
maxBundleSize
number
5242880
Max server bundle size in bytes (5 MB)
rooms.config.json
Room types are defined in a standalone rooms.config.json file at your project root (not the shared config.json):
The file is uploaded with your game when you rundot deploy. The server reads it to register your room types.
Room type fields
type
string
required
Room type identifier used for matchmaking (e.g. "tictactoe", "lobby")
file
string
required
Path to the file exporting the GameRoom subclass, relative to project root
export
string
"default"
Named export of the GameRoom class
singleton
boolean
false
When true, only one room of this type exists. All players join the same room (no matchmaking).
config
RoomConfig
—
Room configuration overrides (see table below)
RoomConfig fields
maxPlayers
number
10
Maximum number of players allowed in the room
idleTimeout
number
300
Time in seconds before an empty room is disposed (5 min)
autoPersist
boolean
true
Whether to auto-persist state on a debounced interval
persistInterval
number
5000
Debounce interval for auto-persist in milliseconds
allowReconnect
boolean
true
Whether to allow reconnections after disconnect
reconnectTimeout
number
30
Time in seconds to hold a player slot for reconnection
startLocked
boolean
false
Whether the room starts locked (no new joins)
metadata
object
—
Custom metadata passed to the room on creation
Multi-room-type apps: Add one entry per game mode:
Quick Start
Server: TicTacToe room
Client: connect and play
Server: GameRoom
Defining messages
GameRoom takes one type parameter:
P— A discriminated union of message types (the protocol). Every member must have a{ type: string }field. This union covers both client-to-server and server-to-client messages.
Lifecycle hooks
All hooks are optional. They can be async or synchronous.
onCreate()
Room is first created. Initialize game data here.
onPlayerJoin(player)
A player requests to join. Call this.reject() to deny.
onGameMessage(message)
A player sends a typed message. message.sender is the player, message.payload is the typed message. Switch on message.payload.type to narrow.
onPlayerLeave(player, reason)
A player leaves. reason is 'leave', 'disconnect', or 'kick'.
onDispose()
Room is about to be destroyed. Final cleanup.
onRestore(snapshot)
Room is restored from a persisted snapshot (crash recovery). State keys are auto-applied before this hook; use it to restore server-only data.
onMigrate(snapshot, oldVersion)
Room is restored but the bundle version changed. Migrate state between versions here.
Messaging
Send typed messages to clients (these arrive via onMessage / onPrivateMessage on the client):
Room control
Persistence
Override getPersistState() to control what gets persisted for crash recovery:
Call this.save() to immediately persist. With autoPersist: true (the default), state is also auto-saved on a debounced interval (persistInterval, default 5000ms).
On crash recovery, onRestore(snapshot) is called with the persisted data. Use it to restore your fields:
Clock
Named timers with auto-cleanup and crash-recovery support:
Timers are automatically serialized and restored on crash recovery. In onRestore, re-register your timers with the same names — the harness adjusts their remaining time automatically so they resume where they left off rather than restarting from zero:
All timers are cleared automatically when the room is disposed.
Logger
Structured logging available on this.log:
Player object
The Player object is passed to lifecycle hooks and available via this.players (a ReadonlyMap<string, Player>):
id
string (readonly)
Unique player identifier (profileId from RUN.game)
username
string (readonly)
Display name
avatarUrl
string | null (readonly)
Avatar URL, if available
joinedAt
number (readonly)
Timestamp when the player joined (ms since epoch)
connected
boolean
Whether the player is currently connected (updates on disconnect/reconnect)
Client: Connecting and Playing
Creating and joining rooms
All methods return a ServerRoom<P> typed with your message union:
Room properties
roomCode
string
Shareable 6-character room code (e.g. "HX9KWR")
playerId
string
The current player's ID
locked
boolean
Whether the room is locked (no new joins)
latency
number
Current latency in ms (round-trip / 2)
connectionState
ConnectionState
'connecting', 'connected', 'reconnecting', or 'disconnected'
Events
Register event handlers with room.on():
All callbacks are optional — only register the ones you need.
Sending messages
Send typed messages to the server room (arrives in onGameMessage on the server):
Leaving
This closes the connection and triggers onPlayerLeave on the server with reason 'leave'.
Server time
Get the estimated server time (local time adjusted by server offset):
Useful for synchronized countdowns or time-based game logic.
Reconnection
Players automatically reconnect with exponential backoff when the connection drops.
Server-side
allowReconnect(defaulttrue) — enables reconnection. When a player disconnects, their slot is held forreconnectTimeoutseconds (default 30).While disconnected,
player.connectedisfalse. The player is still inthis.players— they are only removed when the reconnect timeout expires (triggeringonPlayerLeavewith reason'disconnect').
Client-side
The client fires connection events as the state changes:
onReconnecting
Connection dropped, attempting to reconnect
onReconnected
Successfully reconnected — connection resumes
onDisconnect
Reconnection failed or timed out — connection is closed
Monitor the connection state at any time via room.connectionState:
Best Practices
Use messages for all client updates — broadcast game state changes via typed messages. Use
onPlayerJoinreturn values (joinData) to send initial state to new players.Use typed messages — define a discriminated union for
Pand switch onpayload.typeinonGameMessage. This gives you full type safety and autocompletion.Handle disconnects gracefully — check
player.connectedbefore time-sensitive logic. Skip disconnected players' turns rather than stalling the game.Lock when full — call
this.lock()inonPlayerJoinwhen you have enough players to prevent extra joins during gameplay.Persist strategically — use
this.save()after critical state changes (game start, round end). Rely onautoPersistfor routine saves.Use the clock for timing — prefer
this.clock.setInterval()/this.clock.setTimeout()over rawsetInterval/setTimeoutfor automatic cleanup and crash-recovery support.
Last updated