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
- State & schema — every field type and collection.
- Ownership & the ladder — when to hand writes to the server.
Placeholder walkthrough.