Your first room

We’ll build the simplest possible real feature: shared cursors. Everyone in the room sees everyone else’s pointer. It touches every core piece without any game logic.

Define the state

The schema is the single source of truth for what’s shared and what type it is.

// irtio/schema.ts
import { defineSchema, map, entity, number, string } from '@irtio/schema';

export const schema = defineSchema({
  players: map(
    entity({
      x: number(),
      y: number(),
      name: string(),
    }),
  ),
});

Assign ownership

When someone joins, create their player entity and give them ownership. Owned entities can be written by that client and no one else.

// irtio/room.ts
import { defineRoom } from '@irtio/room';
import { schema } from './schema';

export default defineRoom(schema, {
  onJoin(state, ctx) {
    state.players[ctx.clientId] = {
      x: 0,
      y: 0,
      name: `guest-${ctx.clientId.slice(0, 4)}`,
      owner: ctx.clientId, // only this client may write it
    };
  },
  onLeave(state, ctx) {
    delete state.players[ctx.clientId];
  },
});

Wire up the client

// game.ts
import { joinRoom } from '@irtio/client';
import { schema } from './irtio/schema';

const room = await joinRoom(schema);

window.addEventListener('pointermove', (e) => {
  const me = room.state.players[room.me];
  me.x = e.clientX;
  me.y = e.clientY;
});

room.subscribe((state) => {
  for (const [id, p] of Object.entries(state.players)) {
    if (id === room.me) continue;
    drawCursor(p.x, p.y, p.name);
  }
});

That’s a complete multiplayer feature: typed state, per-client ownership, join/leave lifecycle, and reactive rendering.

Next steps

Placeholder walkthrough.