irtio.json
irtio init writes irtio.json at your project root. It holds four keys, all optional as far as
the file format goes:
{
"project": "p_a5da84132e8d6c2e",
"name": "dive",
"static": { "dir": "dist", "label": "dive" },
"client": "src/main.ts"
} init writes project and name. static and client are opt in, and you add them by hand.
Everything else about a project lives in the control plane, not in this file: allowed origins,
region, JWT signing secrets, deployment versions, limits. There is no port, no endpoint and no
secret in irtio.json, which is why it is safe to commit.
Where it is read from
Every command reads ./irtio.json, relative to the directory you ran it in. There is no search up
the tree and no global config file, so run commands from the project root. --room path/to/room.ts changes which room file gets bundled, not where the config is read from.
-c <file> (or --config <file>) reads a different file instead — see a second project file.
| Command | Reads |
|---|---|
dev | project, client |
deploy | project, client, name |
deploy --static | project, static, name |
migrate create | project |
rollback | project |
logs | project |
rooms, rooms saves, rooms restore | project |
keys jwt-secret, keys jwt-mint | project |
init | project and name, to reuse what is already there |
simulate, login, whoami | nothing |
project
Your project id: the public key that a client presents when it joins, and the project a deploy
lands in. init generates p_ followed by 16 hex characters.
Resolution order differs by command, and only in ways that matter:
| Command | Order |
|---|---|
deploy | --project, then irtio.json, then the project in defineSchema(...) |
deploy --static, migrate create, rollback, logs, rooms, keys | --project, then irtio.json |
dev | irtio.json, then dev |
With no id from any of those sources, control-plane commands refuse by name rather than picking one:
irtio deploy: no project id — pass --project, add "project" to irtio.json, or set it in defineSchema(...) irtio dev is the exception. With no irtio.json at all it runs under the project id dev, which
is also what a browser client falls back to against a localhost endpoint. That pairing is what lets init scaffold a project you can run before you have ever logged in.
The project id is public. It is domain locked by the origin allow list, it is not a credential, and
it belongs in your client bundle and in version control. The credential a project has is its JWT
signing secret, which irtio keys jwt-secret prints once and never writes to disk.
name
The project’s display name: what the dashboard lists it as, and what a deploy registers when it creates the project. It is a label, nothing more. Nothing resolves against it, two projects may share one, and changing it later does not move a deployment.
init writes the directory name. --name overrides it:
npx irtio init --name "Dive Co-op" The file is the source of truth, not a copy of a decision made once:
| Command | Order |
|---|---|
deploy, deploy --static | --name, then the project file, then the directory name |
Nothing is asked. On the deploy that creates the project, this is the name it is registered
under. On any deploy after that, a name that no longer matches the registered one is a rename, applied before anything is uploaded and said out loud:
renamed project p_a5da84132e8d6c2e: dive -> Dive Co-op The directory fallback deliberately does not rename anything. A project file with no name keeps
whatever name the project was created with, because renaming a project on the grounds that its
checkout is called something else would be a surprise. So a project scaffolded before this key
existed needs no change.
The name is a label. Nothing resolves against it, two projects may share one, and a rename does not move a deployment, a room, or the project id.
You can also create the project first, from Projects → New project on the dashboard, which
takes the name and, optionally, an id init already generated. It then prints the irtio.json to
paste back.
The same id also lives in irtio/schema.ts
init writes the id into irtio.json and into defineSchema({ project: ... }), and the two serve
different sides.
Your browser client never sees irtio.json. joinRoom takes the key from schema.project, or from
an explicit key option. The CLI, meanwhile, prefers irtio.json. So if you change the id in one
place and not the other, irtio deploy pushes to one project while your players join a different
one, and the failure shows up as a join refusal rather than a deploy error.
Change both, or delete the id from the schema and pass key to joinRoom yourself.
static
What irtio deploy --static uploads. Two shapes:
"static": "dist"
"static": { "dir": "dist", "label": "my-game" } dir is the built client directory — there is no build step in deploy --static, so run your
own build first. label is the site’s subdomain (my-game.irt-serve.com), and it is the reason
this key is worth setting: the label decides the URL players have, and a deploy that forgets it
publishes the next version somewhere else.
With static set, irtio deploy ships the site along with the room — one command for the whole
project, room first. --no-static deploys the room alone, and --static deploys the site alone:
npx irtio deploy # room, then site
npx irtio deploy --no-static # room only
npx irtio deploy --static # site only A project with no room file and a static key is a valid project — a page that joins someone
else’s room, or a site that is not a game yet. irtio deploy there ships the site and says so
instead of reporting a missing room file.
--static <dir> and --label <label> still win over the file when you pass them. With neither the
flag nor the key, the deploy names both options rather than guessing:
irtio deploy: no directory to upload — pass --static <dir> or add "static" to irtio.json client
An optional path to your client entry, relative to the project root.
It exists for one check: room code must never ship to the browser. Your room file holds server
logic, and in a physics room it holds the exact rules a cheater would like to read. When client is
set, irtio dev and irtio deploy walk that entry’s import graph with esbuild and warn if the
graph reaches the room file.
irtio: WARNING: the client entry src/main.ts imports the room file irtio/room.ts — room code must
never ship to clients; move shared code (e.g. the physics world builder) into its own module both
sides import The walk stays local. Every bare specifier (@irtio/client, react, @dimforge/rapier3d-compat)
is marked external, so it never descends into node_modules and only your own files are considered.
A shared module that both the client and the room import is fine. Only the room entry itself is
flagged.
It is a warning, not a refusal. The check also never fails a build: if the client entry has a syntax error, or the path names nothing, the walk gives up and reports no import, so an unrelated bug in your client cannot block a deploy.
Without client, nothing checks this. It costs one line and it is worth adding, particularly for a
physics room, where the world builder is genuinely shared and the temptation to import the room
alongside it is real.
Limits that live in the control plane
Limits are not in irtio.json. They belong to the project, and the tenant reads them at boot, so
changing one takes effect at the project’s next placement rather than at your next build.
One of them is worth knowing about before you need it.
connectionsPerIpPerMin
New connections allowed from a single client address, per minute. The default is 120, refilling at 2 per second, and it is checked as the socket is accepted, before any frames. It exists to stop one address opening sockets faster than a game ever would, and 120 is generous for real players: a browser tab opens one.
It is not generous for a load run. 200 bots from one machine share one address, so a run of more
than about 120 clients from a single host starts seeing E_RATE_LIMITED with the message too many connections from this address. That is the limit working, not a bug, and raising it for
the run is the intended answer.
Set it on the project:
curl -X PATCH https://control.irt.io/v1/projects/$PROJECT
-H "Authorization: Bearer $IRTIO_TOKEN"
-H 'content-type: application/json'
-d '{"name":"dive","connectionsPerIpPerMin":4000}' name is required by the route and must be the project’s current name. Send "connectionsPerIpPerMin": null to go back to the default. The value reaches the tenant as the
environment variable IRT_CONNECTIONS_PER_IP_PER_MIN at its next start, and the first placement
that carries a value the project did not have before is a cold boot for that project, because the
tenant’s environment is part of what its saved VM image is keyed on. Rooms come back from their own
snapshots as usual; only the boot is slower.
Locally there is nothing to set: irtio dev and startDev({ limits: { connectionsPerIpPerMin } }) take the limit in code, and a local run never crosses a real network anyway.
On a box you run yourself, IRT_CONNECTIONS_PER_IP_PER_MIN on the tenant process is the same knob
with no control plane in the way. A value of 0 is refused at boot rather than silently refusing
every connection.
Validation
| Case | What happens |
|---|---|
| File absent | Fine. dev uses dev; other commands fall back to their next source |
| Not valid JSON | Hard error, naming the file: irtio deploy: /path/irtio.json is not valid JSON |
project is not a non-empty string | dev and deploy refuse by name. logs, rooms, rollback, keys and deploy --static treat it as absent and fall through to their “no project id” error |
name is not a non-empty string | Every command that reads the file refuses by name |
static is not a directory string or an object with a non-empty dir | deploy --static refuses by name |
client is not a non-empty string | dev and deploy refuse by name |
-c names a file that does not exist | Hard error: irtio deploy: no config file at /path/prod.irtio.json. A file you named explicitly is never silently swapped for irtio.json |
| Unknown keys | Ignored. Nothing validates the file as a whole, so a typo like "projet" reads as an absent project |
A second project file
-c <file> (or --config <file>) points a command at a different project file. Every command that
reads one takes it: dev, deploy, deploy --static, migrate create, rollback, logs, rooms and keys.
That is what makes one checkout deploy to more than one project:
// test.irtio.json
{ "project": "p_11112222aaaabbbb", "name": "dive (test)", "static": { "dir": "dist", "label": "dive-test" } } // prod.irtio.json
{ "project": "p_71530c9707fe43ec", "name": "dive", "static": { "dir": "dist", "label": "dive" } } npx irtio deploy -c test.irtio.json
npx irtio deploy --static -c test.irtio.json
npx irtio logs -c prod.irtio.json --follow The path is relative to the directory you run the command in, so -c envs/prod.json works too —
the name *.irtio.json is a convention, not a requirement.
A file named with -c must exist. Falling back to irtio.json when prod.irtio.json is
missing would deploy the wrong project on a typo, so that is a hard error instead.
init has no -c: it always writes irtio.json. Copy that file to make the second one, and
change the id and name in the copy — a second project needs its own project id, which you can
generate by creating the project on the dashboard.
The client bundle is unaffected. joinRoom reads schema.project (or an explicit key), and
nothing in the browser reads a project file, so pointing a build at the test project means passing
that key to joinRoom yourself.
Re-running init
init never overwrites an existing file, irtio.json included, and it lists what it kept:
kept irtio.json (already there — nothing was overwritten) Before writing anything it reads the existing irtio.json and reuses the project and name it
finds, so re-running init in a project that has already been deployed cannot orphan that
deployment behind a freshly generated id, and cannot rename it either. --name does not override an
existing file: nothing is overwritten, so edit the file to change the name — the next deploy applies
it.
A malformed irtio.json is skipped like any other existing file. init does not repair it, and the
project id you get in the newly scaffolded irtio/schema.ts will be a new one, so fix the JSON
first if you are re-scaffolding on top of a broken file.