Presence, chat and parties

Three built-in social features that work without any room code: ask whether a player is online, send text in a room, and hold a group of friends together across several games.

They share one idea. Everything is keyed on the player id your game already has, irt: plus a string, which is what ctx.playerId gives room code and what identity gives the browser. That id is specific to your project. The same person in another game has a different one, so nothing here reaches across projects.

Presence

Presence answers one question: for a player id you already know, are they online.

// main.ts
import { getPresence, watchPresence } from '@irtio/client';

// Ask once.
const who = await getPresence(projectId, [friendA, friendB]);
if (who[friendA]) console.log('friend A is playing right now');

// Or keep asking. The callback runs now, then on every change.
const stop = watchPresence(projectId, [friendA, friendB], (present) => {
  render(present);
});
// later
stop();

A player who is not in a room is missing from the answer. There is no record that says offline, so asking about an id that never existed reads the same as asking about someone who has logged off.

The answer says whether, not where. It does not carry the room a player is in: a room id is how you get into a room, and this is an open route that anyone able to name a player id can call. If you want a follow or spectate button, keep the room in your own storage, where you decide who gets it.

watchPresence is a long poll, not a socket. It holds one request open until the answer changes or the request times out, then asks again. It calls back on the first answer and on every change, and not on a timeout with nothing new. Call the function it returns to stop; the request in flight is cancelled and nothing arrives after that.

How current the answer is

Servers report who is in their rooms every few seconds. A join shows up within about that, and a player who leaves clears within about that plus their reconnect window (reconnectGraceMs, 30 seconds by default): a player who drops keeps their seat, so they are still online while they might still come back.

A server that stops reporting altogether does not leave its players online: a report older than 15 seconds is ignored, so a box that goes away takes its players offline within that.

Presence is a “who is around” signal. Do not build a game rule on the exact moment it changes.

Limits

LimitValueWhen you reach it
Player ids in one query64The query is refused with E_PRESENCE_TOO_MANY. Split it and ask twice
Wait per request60 secondsA longer wait is clamped down to this
Watches open at once, per address4A fifth is refused with E_PRESENCE_TOO_MANY_WATCHES. One watch covers 64 ids, so one is usually enough
Watches open at once, everywhere512Refused with E_PRESENCE_WATCHES_BUSY. Not your doing: read without a wait, and try a watch again shortly
Reads per minute, per address120, with a burst of 30E_RATE_LIMITED
Watches started per minute, per address20, with a burst of 5E_RATE_LIMITED

What presence does not do

There is no friends list, no invites by name, and no blocking. Presence answers about ids you already hold, and where you got them is your game’s business.

There is also no privacy setting. Anyone who can name a player id of your project can learn whether that player is online, and that is the whole of what they learn — not the room, not a name, not an address. Player ids are per-project and hard to guess, and that is the whole of the protection. If your game needs a player to be able to hide, hold that flag in your own storage and do not show them.

Nothing is kept. Every answer is the state right now, and there is no history of who was online when, in your project or anywhere else.

The other meaning of the word

room.clients is also called presence, and it is a different thing: the roster of one room you are already in. Use it to draw the player list inside a game. Use this page’s presence to ask about players from outside any room. See Presence and lifecycle.

Chat

Chat is a built-in text message. It works in every room, including a room with no code of yours.

// main.ts
room.chat.on((from, text) => {
  addLine(room.state.players[from]?.name ?? from, text);
});

sendButton.onclick = () => {
  room.chat.send(input.value);          // everyone in the room
  // room.chat.send(input.value, id);   // one client
};

Chat has its own stream. A chat line never reaches room.onMessage, and a message sent with room.message never reaches room.chat.on, so neither handler is given something it does not expect.

Check a line before anyone sees it

A room with code gets onChat. It runs before the line goes anywhere.

// irtio/room.ts
export default defineRoom(schema, {
  onChat(state, line) {
    // line: { from, text, target }  — target is 'all' or a client id
    if (state.players[line.from]?.muted) return false;   // refuse
    if (line.text.includes(bannedWord)) return false;
  },
});

Returning nothing accepts, so a handler that only wants to log does not have to remember to return. Returning false refuses. Throwing refuses, because a handler that threw has said nothing and the safe reading of silence is no.

A refused line is dropped. Nobody sees it and the sender is not told, so a muted player does not learn they are muted from the game’s own error messages.

Relay rooms have no code, so they have no onChat and every line goes through. If you need to check lines, deploy room code.

No history

Chat is not stored. room.chat.on hears what is said while you are listening and nothing that was said before, and a player who joins late sees nothing. If you want a scrollback, keep the last N lines in your own state or in the browser:

// main.ts
const lines: { from: string; text: string }[] = [];
room.chat.on((from, text) => {
  lines.push({ from, text });
  if (lines.length > 100) lines.shift();
});

Limits

LimitValueWhen you reach it
One line512 bytes of UTF-8Refused with E_CHAT_TOO_LONG. Refused whole, never trimmed
Lines per second, per client1, with a burst of 5Refused with E_CHAT_RATE. The line is not sent, the connection stays open, and you are told once

Counting bytes and not characters matters for scripts outside ASCII: 512 bytes is about 500 Latin characters and about 170 emoji.

Parties

A party is a code a group of friends share so they queue and land together. It outlives one match, so the same party can play several games in a row.

// main.ts
import { createParty, joinParty, readParty, leaveParty, matchRoom } from '@irtio/client';

// One player makes it and reads the code out.
const { party, member } = await createParty(projectId, { size: 4, identity });

// The others take a seat with it.
const joined = await joinParty(projectId, party, { identity });

// Then everyone matches with the same code and their own seat token.
const room = await matchRoom(schema, { party, member: joined.member });

party is the code you read out. member is your seat, and it is a secret: it proves you are in the party. Every call that uses the party needs it, including matchmaking. Without it a call is refused exactly as a wrong code is. Do not show it.

If you call the HTTP API yourself rather than through the client, the seat token goes in the x-irt-party-member header on the party read, and in the body everywhere else. It never goes in a URL, where it would end up in server logs and in the Referer of the next page the browser opens.

What a party does

ThingBehaviour
SizeSet when you create it, 2 to 64. A join past it is refused with E_PARTY_FULL
LeaderThe player who created it. If they leave it passes to whoever has been in longest
Life10 minutes from creation, and joining does not extend it
EmptyThe party ends the moment the last member leaves, and the code stops working

Landing together

Pass the party code and your own seat token to either kind of matchmaking, and the whole group ends up in one room.

// main.ts
// Queue: the queue fills only when the whole party is waiting.
const room = await matchRoom(schema, { party, member, queue: 'ranked' });

// Or join-or-create: land in a public lobby that has room for all of you.
const room = await joinPublic(schema, { party, member, queue: 'casual' });

The first member through picks the room and the rest are answered the same one. A member who is not polling can read it instead:

// main.ts
const view = await readParty(projectId, party, member);
if (view.room) joinRoom(schema, { room: view.room });

A party larger than the queue is refused with E_PARTY_TOO_BIG, naming both numbers.

Someone who joins the party after it has already landed only gets in if the room still has a seat for them. If it does not they are refused with E_ROOM_FULL, and they can match on their own or wait for the next game.

Identity and seats

Pass identity when you create or join and your seat is bound to that player. Reloading the page takes the seat you already had, rather than a second one.

Without an identity there is nothing to recognise you by, so a reload is a new member and your old seat waits out the party’s ten minutes. Use identity for parties. See Player identity.

What a party does not do

There is no friends list and no invites by name. A party is a code somebody reads out, which is why it is short and readable.

The code is not a credential and it does not have to be. Your seat token is the credential: it is what proves you are in the party when you read it, when you leave it, when you queue with it, and when you land with it. Somebody who overhears the code and nothing else is told the code is not valid, the same answer a code nobody ever minted gets. They cannot read who is in your party, take one of its seats, queue as part of it, or send it somewhere its members are not.

Next steps

  • Player identity for the player ids all three of these key on.
  • Quick match for queues, and for join-or-create.
  • Messages for your own message shapes, which are a different thing from chat.