Your game, your code,
in real time.
Multiplayer hosting for web games. Write your rooms in TypeScript, ship them with your site in one deploy, and pay nothing for a room while nobody is in it.
Invite-only while we build it.
Multiplayer hosting on demand.
Every room is a small server that runs your code, on Node in a Firecracker VM that belongs to you alone. It wakes when a player connects and hibernates when the last one leaves.
One command deploy
irt deploy bundles your room code and your site into one build and makes it live. The site and the rooms switch together, so a client always finds the room build it was built against. The dashboard can make an earlier build live again.
Rooms start when players join
A room hibernates 30 seconds after its last connection leaves, and the next connection wakes it with its state exactly as it was. Your code writes no persistence logic for this.
Nothing idle holds a process
Rooms share room hosts, room hosts share a VM, and a VM with no rooms stops after five minutes.
Billed while awake, free while asleep.
Room-hours run from the start of a wake until the room has hibernated, counted outside your VM. In between, the room costs nothing for as long as it sleeps.
- 1
Active
Players are connected and the room runs its ticks. This is the time you pay for.
- 2
Hibernating
The last player left 30 seconds ago. Room state already streams out of the VM as it changes, so hibernating is a flush, not a dump.
- 3
Hibernated
The room's state sits in object storage. No process, no room-hours.
- 4
Waking
A player connects and the router holds them. Everyone held joins at once when the room is back.
Write multiplayer like the rest of your code.
Same language, same types, one project. The client imports the schema from the same module as the room, so a site and its room build always agree.
Declare the room
Typed entities, and handlers for joins, messages and leaves. Each tick, every changed entity is encoded once as a binary delta, however many clients there are.
Room SDK →// shared.ts, imported by the room and the client
import { defineSchema, entity } from "@irt/room";
export const Player = entity("player", {
owner: "string", name: "string",
x: "f32", z: "f32", hp: "u8",
});
export const schema = defineSchema({ entities: [Player] });
// room.ts
import { defineRoom, grid } from "@irt/room";
import { Player, schema } from "./shared.js";
export default defineRoom({
schema,
interest: grid({ cellSize: 32, radius: 2, dims: "xz", around: (c) => c.data.me }),
onJoin(room, conn) {
const { name } = conn.meta as { name: string };
conn.data.me = room.entities.create(Player, { owner: conn.player, name, hp: 100 });
},
onMessage(room, conn, msg) {
const m = msg as { type?: string; text?: string };
if (m.type === "chat") room.broadcast({ type: "chat", text: String(m.text), from: conn.player });
},
onLeave(room, conn) {
room.entities.delete(conn.data.me);
},
});Join from the client
Connect, interpolate remote entities and send messages. A player who connects to a hibernated room sees it wake and then gets its state.
Client SDK →// client.ts
import { connect } from "@irt/client";
import { Player, schema } from "./shared.js";
const room = await connect({
router: "wss://eu.irt.io", project: "my-game", room: "forest-7", schema,
onWaking: () => banner.show("Waking the room"),
});
room.on("restored", () => banner.hide());
const drawn = room.interpolate(Player, ["x", "z"]);
room.entities.onEnter(Player, (p) => scene.add(p.id, p.name));
room.entities.onLeave(Player, (p) => scene.remove(p.id));
scene.onFrame(() => {
for (const p of room.entities.all(Player)) scene.move(p.id, drawn.get(p));
});
chat.onsubmit = (text: string) => room.send({ type: "chat", text });Send each player what they can see
Grid cells for open worlds; teams, zones or dungeon levels when space doesn't split that way. Cap each connection to its closest entities, and patch voxel chunks by range.
State sync →interest: {
...grid({ cellSize: 32, radius: 2, dims: "xz", around: (c) => c.data.me }),
limit: { max: 64, priority: (c, e) => -distance(c.data.me, e) },
},
// A block edit sends a few bytes, not the chunk.
chunk.patch("blocks", offset, new Uint8Array([STONE]));Predict the local player
The client runs the room's own step function, so the local player moves at once. When the room's answer arrives, the client replays the inputs it hasn't confirmed.
Prediction →export const stepper = defineStepper<Input>({
step(world, inputs, dt) {
for (const p of world.all(Player)) {
const input = inputs.get(p.owner);
if (input) move(p, input, dt);
}
},
});
// On the client: sampled once per room tick, replayed on correction.
const me = room.predict(stepper, { find: mine, sample: readKeys });And the hard parts, handled.
Client-side prediction
Tick clock sync, inputs tagged with the tick they are for, and unreliable messages as WebTransport datagrams with a WebSocket fallback.
Crashes and reconnection
A crashed room host restarts its rooms with nothing lost. A room whose host fails restarts on another host, and the router moves its held connections there.
Proximity voice
Each listener hears the speakers it can see, closest first. A speaker's audio is permitted only to its listeners, so a modified client can't overhear anyone.
Sites
Your web client is served at <project>.irt-serve.com. Cross-origin isolation, forSharedArrayBuffer, is one flag.
Regions
Players connect to a router in the same region as their room, and router-to-room-host traffic never crosses regions.
A backend you may not need
Guests, client-created rooms, join codes and public room lists are project settings. When you do have a backend, there's an HTTP API and signed webhooks.
Numbers you can check.
Measured in our spikes. We'll publish production figures once rooms are serving players.
For a VM to restore from its base snapshot and answer, in our Firecracker spike.
Inside a room host that's already running.
After the last player leaves, a room hibernates on its own.
Billed while a room hibernates, however long it sleeps.
Verifiably correct.
@irt/testing runs rooms in process with fake clients and a controlled clock. Catch the bugs multiplayer hides before a build goes live.
With coding agents working on their own, being able to check multiplayer logic matters. A test suite lets an agent check its work before it calls it done, and the output is written for an LLM to read.
See how testing works →- advances ticks on a controlled clock
- hibernates and wakes the room
- crashes the room host and restarts it
- asserts on messages and room state
Your agent already speaks IRT.
Multiplayer is hard for agents: lots of moving parts that aren't normally testable. IRT gives your agent the docs and the tools to get it right without a human in the loop.
Try it yourself:
Docs written for agents
The docs are published as llms.txt, as one file atllms-full.txt, and every page as Markdown.
Verified without a human
Agents can't open two browsers and click, but they can run fake clients against a room and read the result.
Examples that compile
Every TypeScript example in the docs and on this page is compiled against the real packages, so the code your agent copies is the code that works.
No vendor lock-in.
We aim to be the best place to host your game, but you can run it wherever you like.
Your code is yours
Room code is ordinary TypeScript on Node, and the client SDKs are open source.
Host it yourself
The room host is open source and runs on its own: it accepts WebSocket connections directly and has pluggable storage.
Same code both ways
The client SDKs work against a self-hosted room host as well as against our router.
One dashboard for your live game.
- Builds. Every build you've deployed, and the one that's live. Make an earlier one live again.
- Rooms. The rooms running now. Read a room's state, which wakes it if it's hibernated.
- Logs. Room host output from the last 15 minutes, then live. Also in your terminal with
irt logs. - Usage. Room-hours against spending limits per day, week and month.
Dead-simple pricing.
Pay for usage from prepaid credit, with no monthly fee yet. Usage prices will be published before launch.
Room-hours
One room awake for an hour, however many players are in it. Priced by room size: relay, standard, large or extra large.
Traffic
Counted by the router, outside your VMs.
Voice-minutes
Only if you use voice chat.
Room state
What your hibernated rooms keep in object storage.
No CCUs. Billed only on what we can measure.
Instead of per-player pricing, every room is a tiny server and you pay for the time it's awake. Code inside a VM could report anything, so everything we bill is counted outside it.
Every project has spending limits per day, week and month, so a bug that creates rooms in a loop can't run up a bill.
Prepaid credit, topped up by card.
Usage is paid from credit you buy in advance through Stripe, so there's no surprise invoice. Top up $10, $25 or $50, or any whole amount from $5 to $500; tax is added at checkout where it applies. Every invite comes with $10 of credit to start.
If your credit runs out, your projects keep running for 24 hours. After that they stop waking rooms and running rooms hibernate until you top up. Running out never deletes a room.
Free while you try it, by invite
We're inviting web game developers in small groups, and may give you credit to try IRT. Free projects run one room at a time with fewer connections.
Your game, your code, in real time.
Tell us what you're building. Email hello@irt.io with your game and your GitHub username.