Build and deploy

Deploying with the CLI

Take a project from login to live with irt login, init, dev, test, build, deploy and logs.

The irt CLI covers the whole loop: log in, create a project, run it locally, test it, deploy it and read its logs.

Install

The CLI is the npm package @irt/cli (the name irt on npm belongs to someone else). It needs Node 24 or later. Run it without installing:

npx @irt/cli <command>

or install it, globally or as a project devDependency, for the irt command:

npm install -g @irt/cli
irt <command>

Every error is one line that says what to do, and the CLI exits with a non-zero status. irt help lists every command, and irt <command> --help one command’s arguments and flags.

Global options

Option or variable Meaning
--project <slug> The project to act on; overrides project in irt.config.ts
IRT_TOKEN A token to use instead of the stored login; set it to a project API key (irt_key_...) in CI
IRT_API_URL The API base URL; default https://api.irt.io/v1

irt.config.ts

A project declares its config and settings in code, next to its room:

// irt.config.ts
import { defineConfig } from "@irt/cli/config";

export default defineConfig({
  project: "space-race",
  room: "src/room.ts",
  site: { dir: "dist", build: "npm run build:web", crossOriginIsolated: false },
  settings: {
    players: { guests: true, createRooms: true, listPublic: true },
    voice: { enabled: true, maxSpeakers: 8 },
    rooms: { size: "standard", retention: "7d", buildUpdate: "hibernate" },
  },
});
Field Meaning
project The project’s slug: a-z, 0-9 and -, its name in URLs and in <slug>.irt-serve.com
room The room module; its default export is the result of defineRoom. It’s one room type, named default. Give room or rooms
rooms The project’s room types by name; see Room types
site.dir Optional: the directory of built site files that’s deployed
site.build Optional: a shell command that builds the site into site.dir, run by irt build, irt deploy and irt dev
site.crossOriginIsolated Optional, default false: serve the site with COOP and COEP
settings Optional: the project’s settings, below

The CLI bundles the file with esbuild, loads it, and checks it before building, so a bad value fails in one line naming the field and what it allows, before anything is uploaded:

irt: irt.config.ts: settings.rooms.retention is "2y"; it must be "none", a number of hours or days such as "12h" or "7d" up to 365 days, or "forever"

Room types

A project whose rooms run different code, such as a lobby and a match, declares each as a room type in rooms instead of room:

// irt.config.ts
import { defineConfig } from "@irt/cli/config";

export default defineConfig({
  project: "space-race",
  rooms: {
    default: "src/lobby.ts",
    match: { room: "src/match.ts", size: "large", maxPlayers: 8, retention: "1d" },
    chat: { native: true, size: "small" },
    world: { native: true, schema: "src/world.ts", tickRate: 20, size: "small" },
  },
});
Entry Meaning
"path" The type’s room module, whose default export is the result of defineRoom
{room, size?, maxPlayers?, retention?} The module, with the type’s own room size, cap on connections and retention, over the project’s rooms.size, none and rooms.retention
{native: true, schema?, tickRate?, lobby?, size?, maxPlayers?, retention?} A native room, which runs no code of yours: it sends each message on to the other players and keeps presence and a leader. With schema, players also spawn, own and write the entities of the schema module’s default export, under the rules each entity() declares, and it ticks tickRate times a second, 1 to 60, 20 by default. With lobby, it gathers players and hands them off to a room of another type; see Lobbies and parties

A project has 1 to 32 types, each named with 1 to 32 characters of a-z, 0-9 and -, starting with a letter. A standard type can’t be small until small has a price; a native type can. maxPlayers is 1 to its size’s connection limit. A create, quick join or connection that names no type gets the type named default; without one, every one of them must name a type. Clients pass the type to connect, createRoom, quickJoin and listPublic as type.

Settings

settings has the same names as the project’s settings in the HTTP API and the dashboard:

Setting Default Allowed Meaning
players.guests true boolean Clients may join with no player token
players.createRooms true boolean Clients may create rooms and use join codes
players.listPublic true boolean Clients may list active public rooms
players.quickJoin false boolean Clients may join any public room with space, or a new one
players.listPublicWithoutToken false boolean Anyone may list public rooms with no token, even with players.guests off
voice.enabled false boolean Connections get a voice token
voice.maxSpeakers 64 integer 1 to 64 A ceiling on each room’s own voice.maxSpeakers, applied from a room’s next wake; see Proximity voice
rooms.size "standard" "standard", "large", "extra-large" Room size for new rooms
rooms.retention "7d" "none", "<n>h", "<n>d" up to 365 days, "forever" How long a room is kept after it hibernates; "none" deletes it when it empties
rooms.buildUpdate "hibernate" "restart", "hibernate" Running rooms move to a new build at once, or on their next hibernate
rooms.rollbackOnCrashLoop true boolean When a newly live build crash-loops, make the build it replaced live again, once per build; see Builds
data.player.clientWrite [] player data keys The keys a client may write for its own player
data.collections.<name> none 1 to 32 of a-z 0-9 _ -, from a letter; at most 32 A collection, with clientRead (boolean, default false), clientWrite ("own", default none), maxOwn (1 to 10,000, default 100) and leaderboard (order "desc" or "asc", keep "best", "latest" or "sum", periods of "all", "day", "week", "month", default ["all"]); see Player data

irt deploy sends the settings in the build. When the build goes live, the settings it declares become the project’s, and rolling back to an older build rolls them back too. A setting the live build declares is read-only in the dashboard. A setting the config leaves out keeps its current value. limits and webhookUrl never come from code: they’re the account owner’s, so a deploy can’t raise a spending cap, and defineConfig rejects them.

irt login and irt logout

irt login
irt logout

irt login logs the CLI in with a device code:

  1. The CLI shows a short code and opens https://app.irt.io/device.
  2. You sign in to the dashboard with GitHub, if you aren’t already, and enter the code.
  3. The CLI receives a personal token and stores it in ~/.config/irt/credentials.json, readable only by you (mode 600). The token acts for your tenant.

irt logout deletes the stored token. In CI, don’t log in: set IRT_TOKEN to an API key from the dashboard, which acts for one project only.

irt init [dir] [--project <slug>]

irt init my-game --project space-race

Creates a directory (default: the current one) with a minimal working game for the project --project names. Without --project, the slug is made from the directory’s name: lowercased, with runs of other characters turned into -. init prints the slug it used and what to run next:

created /home/you/my-game for the project space-race
the slug is `project` in irt.config.ts, which web/main.ts imports
next: cd my-game, npm install, npx @irt/cli projects create space-race, then npx @irt/cli dev

When you’re logged in, init asks the API whether the project exists and leaves projects create out of the next steps if it does. Logged out, or when the API can’t be reached, the step reads npx @irt/cli projects create space-race if it doesn't exist yet.

To change the slug later, edit project in irt.config.ts; the scaffolded web/main.ts imports it from there. Create the project itself with irt projects create <slug>.

File Contents
irt.config.ts The project’s slug, entry points and settings
build-web.mjs Bundles and minifies web/main.ts into dist/ with esbuild, for site.build, with each dynamic import() in a chunk of its own, and hashed file names that it writes into dist/index.html
src/schema.ts The schema, shared by the room and the client
src/room.ts The room code
src/room.test.ts A test with @irt/testing
web/index.html, web/main.ts A plain TypeScript web client with no framework

The generated irt.config.ts declares every setting at its default. irt dev applies the settings locally: guests, room creation and public listing as declared, and rooms.retention: "none" deletes a room when it empties. Voice is off locally unless IRT_LIVEKIT_URL names a LiveKit server; see Proximity voice.

irt dev [--port 7350]

Runs the project on your machine: irt-room-host in local mode with each room type’s bundled room code, native types included, serving the client protocol and the site on one origin, http://127.0.0.1:<port>, with the same headers the hosted site gets. The scaffolded client uses location.origin as its router address there, and wss://eu.irt.io on irt-serve.com. That includes COOP and COEP when the site is cross-origin isolated, so SharedArrayBuffer behaves locally as it does on irt-serve.com. It rebuilds and restarts the room host when a file in the project changes, so connected clients reconnect, and says which file it was. Without a site.build command, only the room’s own files and site.dir count; with one, any file can, since the command may read it. It doesn’t watch dot files and directories, such as .irt and .git, node_modules, the site build’s site.dir, *.log files or editors’ swap files, so irt dev > dev.log inside the project doesn’t rebuild it over and over. Ctrl-C, SIGTERM and SIGHUP stop the room host with it; if the port is taken, as by a room host left over from an irt dev that was killed with SIGKILL, it says so and exits. Room state is kept in .irt/data between runs. The inspector at /v1/dev/ shows each room’s connections, state, entities, recent messages and calls, and tick profile; see Dev tools.

Flag Default Meaning
--port 7350 Port of the local room host and site

In local mode, as deployed, a room is created when a client first connects to it, if players.createRooms is on.

irt test

Runs the project’s tests with Vitest. Tests use @irt/testing, which runs rooms in process with test clients and a controlled clock; its failure messages say what was expected, what happened, and in which room, connection and tick.

irt test doesn’t typecheck, since Vitest strips types without checking them, so a test run stays fast. irt build and irt deploy do; to check types while you work, run npm run typecheck, the scaffold’s tsc --noEmit.

irt simulate

Runs the project’s room locally, in process, with simulated players: each a bot with a real @irt/client connection, driven by the built-in behaviours or by the project’s irt.sim.ts. After every tick it checks invariants: no client sees an entity its interest excludes, predictions aren’t corrected too often, owned writes aren’t silently lost, the room logs no errors, room state stays within limits, reconnects restore the room’s state, and the script’s own. It prints a line per invariant with traffic per player and time per tick, writes a trace of JSON lines, and exits with 1 when an invariant fails. It never runs against a deployed project. See Simulating players.

Flag Default Meaning
--players 8 Bots that join before the first tick
--ticks, --seconds 30 seconds How long to run, in ticks or seconds of room time
--seed 1 Seeds the bots, the network and Math.random in room code
--script irt.sim.ts if it exists The simulation script
--behaviour walk Built-in behaviours for every bot: walk, chat, idle, or several joined by commas
--rtt, --jitter, --loss none A simulated network for every bot: round trip and jitter in milliseconds, and the fraction of unreliable messages lost
--drops 0 Connections to drop at random ticks, to check reconnects
--trace .irt/simulate/trace.jsonl Where to write the trace
--type default The room type to simulate, without a script; a script names its own room

Flags override the script’s own settings.

irt build [--out dir] [--no-typecheck]

Typechecks the project, bundles the room module with esbuild into one file, runs the site’s site.build command if irt.config.ts has one, and writes the build tarball. It prints each room type’s schema hash:

built .irt/build/build.tar.gz (9 files)
  room type default: schema 0x1f2e3d4c
  room type match: schema 0x5a6b7c8d, large, max 8 players, retention 1d
  room type chat: native, schema 0x9437de34, small

The typecheck is tsc --noEmit with the project’s tsconfig.json, so it covers whatever that file includes: the scaffold’s covers src, web and irt.config.ts. Any error fails the build after printing tsc’s errors. A project without tsconfig.json isn’t checked, and one with it but no typescript installed fails until you install it. --no-typecheck skips the check, such as to deploy a fix while an unrelated file has errors.

The tarball:

Path Contents
irt-build.json {format: 2, sdk, site: {crossOriginIsolated}, settings, types}, with each room type’s {entry, schemaHash, schema, stateVersion} or {native, schemaHash, schema, stateVersion, tickRate, lobby?}, lobby with every default filled in, and its own size, maxPlayers and retention; schemaHash is 0x and 8 hex digits, stateVersion is the type’s version. See Builds
room/ Each standard room type’s bundled room code, as {type}.mjs
site/ The site’s files, if the project has a site
Flag Default Meaning
--out .irt/build Directory to write build.tar.gz to
--no-typecheck off Build without running tsc

The unpacked files stay in stage/ under that directory, which irt-room-host runs directly; see Self-hosting.

irt deploy [--no-live] [--no-typecheck]

Builds as irt build does, typecheck included unless --no-typecheck is given, uploads the build to the project, and makes it live unless --no-live is given. Then it prints where the site is served and the router address its clients pass to connect, from the project’s client config:

deployed build b_01J9Z3 to space-race; it's live
room state of type default: compatible, at version 1; rooms wake from the live build's room state without a migration:
  added player.score (u32); room state without it loads with its default value
  site:   https://space-race.irt-serve.com
  router: wss://eu.irt.io (pass it to connect() as `router`)

The room state of type lines are the control server’s schema check: how each room type’s schema and version compare with the same type in the build it replaces, and each change. A build whose changes old room state can’t load, at the same version, is refused. It stays uploaded, the live build stays live, and the CLI lists each change and exits with 1:

build b_01J9Z4 isn't live; these changes to room type default's room state need a version bump and a migration:
  removed player.hp (u8), so room state that has it won't load
irt: build b_01J9Z4 can't replace the live build b_01J9Z3: it changes room state without a version bump, both builds being at version 1: removed player.hp (u8), so room state that has it won't load. Bump `version` in defineRoom to 2 and add a migration, `migrations: { 2: (data) => ... }`, that converts room state from version 1

There’s no flag to skip the check: rooms would fail to wake on the build. A build that drops a room type while rooms of it have records is refused the same way, naming the type; see Adding and removing room types.

Making a build live does two things at once:

  • the site at <slug>.irt-serve.com switches to the build’s site, and
  • rooms move to the build’s room code according to the project’s buildUpdate setting: on their next hibernate, or with restart straight away, a few a second on each host. See New builds.

Because the site and room code go live together, a client always finds the room build it was built against. A client loaded from an older build that connects to a room on a newer one gets close code 4004 and should reload. A room still on an older build refuses newer clients with 4004 too, until it hibernates and moves. See Builds and deploys for the schema hash rule, migrations and rollbacks.

A site marked cross-origin isolated in irt-build.json is served with COOP and COEP. SharedArrayBuffer needs them, and they stop the site loading cross-origin resources that don’t opt in. See Sites.

The dashboard lists builds and can make an earlier build live again, as irt rollback does.

irt rollback [build-id]

irt rollback
irt rollback b_01J9Z3

Makes the build before the live one live again, or the build you name. The site and the build’s declared settings switch at once, and running rooms move to it as rooms.buildUpdate says. The CLI prints which:

rolled space-race back to build b_01J9Y8; it's live, with its site and its declared settings
  room type default: schema 0x1f2e3d4c
running rooms stay on build b_01J9Z3 until they next hibernate, then wake on build b_01J9Y8 (rooms.buildUpdate is hibernate)

When the older build has an older room state version, rooms that migrated past it can’t run on it, and the CLI prints a room state of type default: version 2 to 1, a rollback line saying so. Each such room wakes from the room state kept from before its migration, losing progress since then, and your backend gets a room.restored webhook. Room state from before a migration is kept for 7 days. A room with none kept, such as one created on the newer build, fails to wake until a build at its version is live again; irt logs says why.

A rollback goes through the same schema check as a deploy. Rolling back to a build at the same version that lacks a field or kind the live build added is refused, since rooms that woke on the live build saved room state with it. Deploy a new build instead.

irt restart [room-id]

irt restart
irt restart forest-7

Moves the project’s running rooms, or the one named, onto the live build now, whatever rooms.buildUpdate says. Each room hibernates, saving its room state, and wakes on the live build, even when it runs that build already, so a restart also clears whatever a room host held in memory. Clients close with 4008 and reconnect at once. Rooms move 20 a second on each host. Hibernated rooms aren’t touched: they wake on the live build anyway. Naming a room that isn’t running is an error.

irt logs [--follow]

Prints the output of the project’s room hosts; with --follow, keeps printing as new lines arrive. The same stream is in the dashboard. It holds what room code prints, errors the room SDK logs, such as a hook that threw, and the host daemon’s notes about a room, such as why it can’t wake or that its room state nears 32 MB. A room that runs without printing or failing writes nothing, and irt logs then says it found no lines. Each host keeps the last 1 MB of a project’s lines, in memory.

irt rooms

irt rooms list [--public] [--all]
irt rooms show <id>
irt rooms profile <id>
irt rooms create [id] [--type <room type>] [--public] [--join-code] [--retention <value>]
irt rooms delete <id>
irt rooms send <id> '<json>'

profile prints an active room’s tick profile over the last second, as its room host measured it: per phase, such as stepper, onTick, interest and flush, how many ran and their p50, p95 and max in milliseconds. It doesn’t wake a hibernated room.

list prints each room’s ID, status and players. It lists only active rooms, so a room that emptied and hibernated drops out of it until it wakes; use --all to list every room the project keeps, with hibernated ones shown as hibernated. --public lists only public rooms. show prints one room’s status, players, build, size and room state size as last saved to object storage, which for a running room trails by up to 30 seconds. create --type sets the new room’s room type, default default. create --retention gives the room a retention of its own, such as forever or 30d, which wins over the project’s rooms.retention; see Retention. send delivers a server API message to the room’s onRequest and prints the reply. A message to a hibernated room wakes it.

irt secrets

irt secrets list
irt secrets set <name> <value>
irt secrets unset <name>

Secrets reach room code as environment variables, read with process.env. A room host reads them when it starts, so a changed secret reaches rooms on room hosts started after the change: irt deploy starts new ones for the new build, and rooms move to them as the project’s rooms.buildUpdate setting says. Their values are never shown again after they are set; list shows names only. Names are 1 to 64 letters, digits and underscores; PATH, NODE_OPTIONS, HTTPS_PROXY, NODE_USE_ENV_PROXY and names starting with IRT_ are reserved for IRT.

irt bans

irt bans list
irt bans add <player> [--reason <text>] [--for <30m|12h|7d>]
irt bans remove <player>

add bans a player from every room of the project, permanently or for --for minutes, hours or days. Their connections close with 4013 within a few seconds. See Moderation.

irt projects

irt projects list
irt projects create <slug> [name]
irt projects delete <slug> --yes

Deleting a project stops its site and rooms for good. Its slug is never reused, not even by you, so delete asks for --yes.

irt api-keys

irt api-keys list
irt api-keys create <name>
irt api-keys delete <id>

API keys act for one project, for backends and CI. create prints the key once; it can’t be shown again. list shows each key’s ID, name and first characters.

irt signing-keys

irt signing-keys list
irt signing-keys create
irt signing-keys delete <id>

Signing keys sign player tokens. create prints the new key’s ID and its secret once; it can’t be shown again, so store it on your backend. list shows each key’s ID and when it was created. A project has at most 5. Deleting a key stops the player tokens it signed from connecting; to rotate, create a new key, move your backend to it, then delete the old one. These commands need irt login: an API key can’t manage signing keys.

irt webhooks

irt webhooks show
irt webhooks set <url>
irt webhooks unset
irt webhooks rotate-secret

set and unset change the project’s webhookUrl, where webhook events go. rotate-secret makes a new signing secret and prints it once; it signs every event from then on, so update your backend straight away. Events are sent only once the project has both a URL and a secret. These commands need irt login: an API key can’t change where events go.

irt whoami, irt status and irt usage

irt whoami
irt status
irt usage [--from 2026-11-01] [--to 2026-11-30]

whoami shows the user, tenant and credit balance the login acts for. status shows the project, its live build with its room types, the latest build that was rolled back for crash-looping and why, and its active rooms and players. usage prints room-hours by room size, traffic and cost by day, and the total; dates are UTC and inclusive, and default to the last 30 days.

irt stats

irt stats [--from 2026-11-03T00:00:00Z] [--to 2026-11-04T00:00:00Z]
irt stats --build <id> [--from <time>] [--to <time>]

Prints each build’s peak players and rooms, reconnects, and the errors, ticks and average and longest tick that its rooms report, over a range of at most 14 days, by default the last 24 hours. With --build, it prints that build’s numbers minute by minute, over at most 24 hours, by default the last hour. Times are RFC 3339, in UTC. These are the numbers of Stats and usage alerts. Tick cost there is per build; per room and per hook it’s only in the irt dev inspector.

irt skill install [--dir <path>] [--force]

irt skill install

Writes the IRT skill for coding agents, the integration rules, common mistakes and which irt commands to run, to .claude/skills/irt/SKILL.md in the project: the nearest directory with irt.config.ts, or the current one. Claude Code loads it when it works on the project. --dir writes SKILL.md into that directory instead, such as ~/.claude/skills/irt for every project.

A file that’s already the same is left alone. One that differs, because you edited it or an older CLI wrote it, isn’t replaced unless you pass --force. The skill points agents to the guide for coding agents.

irt mcp [--allow-writes]

claude mcp add irt -- npx -y @irt/cli mcp

Runs an MCP server on stdio for coding agents, with tools that search and read the IRT docs and read the project’s status, builds, rooms, room state, logs, usage and bans with your login. --allow-writes adds tools that create and delete rooms, send rooms messages, make builds live and ban players. See MCP server for setup in other clients and the full tool list.

Without the CLI

Everything the CLI does goes through the HTTP API, so a CI pipeline can upload builds and make them live with an API key.

View this page as Markdown