Build and deploy
Moderation
Kick and ban players from room code, ban them from a project, and kick or mute them from your backend, the dashboard or the CLI.
A room can kick a connection and ban a player from itself in room code, which suits games where players run their own rooms. To act from outside a room, from your backend, the dashboard or the CLI, use bans, kicks and voice moderation through the HTTP API. Each takes an API key, a personal token or a dashboard session.
Players are named by their player ID: your player
token’s sub, or a guest’s guest: ID. A room
sees it as connection.player.
In room code
| Call | Close code | Kept | Scope |
|---|---|---|---|
room.kick(conn, reason?) |
4006 | no; the player can join again | one connection |
room.ban(player, { reason?, expiresAt? }) |
4014 | in room state, across hibernation | this room |
| A project ban, below | 4013 | by the control server | every room of the project |
The client sees the code as IrtError.code and the room’s reason as
IrtError.reason, so it can say why. A banned player is turned away
before onJoin runs, and never shows in other clients’ presence; so is a
connection the room kicks in onJoin before any await.
This room lets its owner, the player whose token says so, kick and ban, and turns banned players away with a message:
// moderated-room.ts
import { CallRejected, defineCalls, defineRoom, defineSchema, entity } from "@irt/room";
const calls = defineCalls<{
ban(args: { player: string; reason: string; hours?: number }): null;
unban(args: { player: string }): boolean;
}>();
const schema = defineSchema({ state: entity("lobby", { topic: "string" }), calls });
const isOwner = (meta: unknown) => (meta as { owner?: boolean } | null)?.owner === true;
export default defineRoom({
schema,
calls: {
ban(room, conn, { player, reason, hours }) {
if (!isOwner(conn.meta)) throw new CallRejected("only the room's owner can ban");
room.ban(player, { reason: reason.slice(0, 100), expiresAt: hours ? Date.now() + hours * 3600_000 : undefined });
return null;
},
unban(room, conn, { player }) {
if (!isOwner(conn.meta)) throw new CallRejected("only the room's owner can unban");
return room.unban(player);
},
},
});
On the client, tell kicked and banned players apart:
// closed.ts
import { CloseCode, type RoomClient } from "@irt/client";
export function onClosed(room: RoomClient, show: (text: string) => void) {
room.on("closed", (e) => {
if (e.code === CloseCode.RoomBanned) show(`You're banned from this room${e.reason ? `: ${e.reason}` : "."}`);
else if (e.code === CloseCode.Kicked) show(`You were removed${e.reason ? `: ${e.reason}` : ""}. You can rejoin.`);
else if (e.code === CloseCode.Banned) show("You're banned from this game.");
});
}
To hide players from each other, such as after a block, give the room
seesPresence(room, viewer, entry) beside an interest sees: presence
then follows the same rule as entities. See
Presence.
Room code mutes a player’s voice with room.muteVoice(player, { expiresAt }), kept in room state like a ban: the room leaves the player
out of every listener’s speakers, and the voice server stops it
publishing until the mute ends. See
Proximity voice. Room
code has no call for project bans; call the HTTP API from your backend.
Bans
A ban keeps a player out of every room of the project, for good or until it expires.
curl -X POST https://api.irt.io/v1/projects/my-game/bans \
-H "Authorization: Bearer $IRT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"player": "u_42", "reason": "cheating", "expiresAt": "2026-11-11T00:00:00Z"}'
- The router refuses a banned player’s connections with close code 4013, which the client SDK doesn’t retry. Connections the player has when the ban is made close with 4013 within a few seconds.
- With a banned player’s token, the client HTTP API’s room creation,
join code lookup and public room list answer
403with codebanned. expiresAtis optional; without it the ban is permanent. Once it passes, the player can connect again.- Banning a player again replaces the ban’s reason and expiry.
- A project can have 10,000 active bans. Expired bans don’t count.
Banning a guest keeps out only that guest ID, and a guest gets a new one by clearing the browser’s storage. A game that needs bans to stick should sign its own player tokens and turn guests off.
On the client, tell a banned player why they can’t join:
// banned.ts
import { CloseCode, connect, defineSchema, entity, IrtError } from "@irt/client";
const schema = defineSchema({ state: entity("table", { players: "u8" }) });
try {
const room = await connect({ router: "wss://eu.irt.io", project: "my-game", room: "main", schema });
room.on("closed", (error) => {
if (error.code === CloseCode.Banned) console.log("you were banned from this game");
});
} catch (e) {
if (e instanceof IrtError && e.code === CloseCode.Banned) console.log("you are banned from this game");
else throw e;
}
Kicks
A kick closes a player’s connection to one room with close code 4006,
the same as room.kick. The client doesn’t reconnect by itself, but the
player can join again; ban them to keep them out.
curl -X POST https://api.irt.io/v1/projects/my-game/rooms/forest-7/kick \
-H "Authorization: Bearer $IRT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"player": "u_42"}'
It answers 204, or 404 when the player isn’t connected to the room.
Voice
With proximity voice on, a player’s voice can be muted for everyone in a room, or the player disconnected from the room’s voice:
| Path | Body | Does |
|---|---|---|
POST /projects/{p}/rooms/{id}/voice/mute |
{player, muted} |
Mutes or unmutes the player’s microphone |
POST /projects/{p}/rooms/{id}/voice/remove |
{player} |
Disconnects the player from the room’s voice |
Both answer 204, or 404 when the player isn’t in the room’s voice.
The mute applies to the player’s tracks as they are, and its client can
unmute them; for a mute that holds, have the room call room.muteVoice.
Removing a player from voice doesn’t stop them rejoining it with their
voice token; kick or ban them for that.
In the dashboard and the CLI
The dashboard’s Bans tab lists a project’s bans and adds and removes them, and each active room on the Rooms tab has a Moderate panel for kicks and voice. In a terminal:
irt bans list
irt bans add u_42 --reason cheating --for 7d
irt bans remove u_42