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:
- The CLI shows a short code and opens
https://app.irt.io/device. - You sign in to the dashboard with GitHub, if you aren’t already, and enter the code.
- 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.comswitches to the build’s site, and - rooms move to the build’s room code according to the project’s
buildUpdatesetting: on their next hibernate, or withrestartstraight 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.