This guide gets you from nothing to two players exchanging messages in a shared room. You need an app key from the Plot dashboard (the free tier needs no credit card).
1. Install the client
npm install -E @plot/clientOne package, no peer dependencies.
2. Join a room
import { Plot } from '@plot/client';
const plot = new Plot({
appKey: 'pl_pub_live_xxx', // from your Plot dashboard
playerId: crypto.randomUUID(),
});
const room = await plot.join({ roomCode: 'lobby-1' });
room.on('message', ({ from, data }) => console.log(from, data));
room.on('join', ({ playerId }) => console.log('joined:', playerId));
room.on('leave', ({ playerId }) => console.log('left:', playerId));
room.send({ x: 100, y: 200 });Open the same roomCode in two tabs and each will see the other’s messages and
presence events. That is the whole v1a relay surface — no server code required.
Multiple apps? If your account has more than one app, also pass
appId:new Plot({ appKey, appId }). The client sends it as theX-Plot-Appheader so requests route to the right app. With a single app it’s optional.
3. Add server logic (optional)
When you want the server to own state — validating moves, running a tick loop, keeping a score — define a room handler and deploy it from the Handlers page in your dashboard :
import { defineRoom } from '@plot/handler';
export default defineRoom<{ scores: Record<string, number> }>({
initialState: { scores: {} },
onJoin(player, ctx) {
ctx.state.scores[player.id] = 0;
ctx.broadcast('event', { type: 'joined', id: player.id });
},
onMessage(player, msg, ctx) {
if (msg.type === 'point') ctx.state.scores[player.id] += 1;
},
});The client doesn’t change — it still joins and sends. The difference is the
server now decides what happens. See Authoritative handlers.
4. Real-time games — one call
For an action game you want client prediction, smooth remote interpolation, and
auto-reconnection. Rather than wiring those by hand, plot.play() manages them —
you declare which state paths are local (predicted) vs remote (interpolated)
and draw in onFrame:
const game = await plot.play({
room: 'lobby-1',
handler, // the same defineRoom, replayed locally for prediction
predict: [{ path: 'positions.{me}', type: 'vec2' }], // you — predicted + corrected
interpolate: [{ path: 'positions.*', type: 'vec2' }], // others — interpolated
onFrame: ({ me, local, remote }) => draw(local[`positions.${me}`], remote),
});
game.input({ kind: 'move', dx, dy, dt }); // predicted sendPrediction, interpolation, keepalive, reconnect-with-resume, and a 60fps frame
loop are all on by default. The full game is the starter example
(a handler plus this one call). See plot.play() for the options.
Next steps
plot.play()— the managed client, in full.- Channels —
state/event/chat/unreliable. - Persistence — profiles, leaderboards, saves.
- Snapshot interpolation — smooth remote movement.