Build Games Where Everyone Has A Part

Plot turns any web game into a multiplayer experience that unfolds across every player's phone or computer. Build with TypeScript or JavaScript, use any browser framework, and own every rule, screen, and surprise.

One await Plot.ready() gives every game copy its current Plot.party, shared Plot.gameState, per-player Plot.playerData, typed Plot.realtime.channels, and synchronized Plot.time clock. Connection and recovery stay behind the SDK. Use Plot.onLifecycleChange() to pause during an interruption and rebuild from complete state when connected, before retained historical channel events. Focused state listeners handle ordinary data changes.

Your first realtime game

import Plot from '@plot.game/sdk'

// The host updates Game State; every player can read it.
// Subscribe once; changes, late joins, and reloads stay in sync.
type GameState = { roundEndsAt?: number }
const gameState = Plot.gameState.as<GameState>()
gameState.subscribe((state) => {
  renderGame(state)
})

// Player Data gives each player one object only they can write.
// Every screen can read every object: ideal for choices and scores.
type PlayerData = { ready?: boolean }
const playerData = Plot.playerData.as<PlayerData>()
playerData.subscribe((data) => {
  renderReadiness(data.objects)
})

// Typed channels carry moments happening now.
// Target one player or broadcast; Plot handles delivery and recovery.
type Buzz = { pressedAt: number }
const buzzes = Plot.realtime.channels.channel<Buzz>('round.buzz')
buzzes.subscribe((message) => {
  showBuzz(message.fromPlayerId, message.payload.pressedAt)
})

// One call activates a fully synchronized player.
// Connection and recovery stay inside Plot.
await Plot.ready()

// Party provides full player objects and stable IDs.
// Subscribe once for live presence and profile updates.
const party = Plot.party.get()
const self = party.self
const host = party.host
Plot.party.subscribe((updatedParty) => {
  renderParty(updatedParty.players)
})

// Each player writes only their object; all players see the update.
await playerData.setSelf({ ready: true })

// Plot.time is a synchronized clock with a shared epoch.
// Use it to schedule events, run countdowns, and timestamp actions.
async function startRound() {
  if (self.playerId !== host.playerId) return
  await gameState.set({
    roundEndsAt: Plot.time.now() + 30_000,
  })
}

function buzzIn() {
  buzzes.sendTo(host.playerId, { pressedAt: Plot.time.now() })
}

Each browser gets stable player identity, player-owned data, durable shared progress, targeted live events, and one shared clock. Plot keeps late joins, reloads, and reconnects synchronized.

Where to go next