Getting Started
ARRR (Agentic Real-time Relay Routing) delivers everything a room's clients send to every client in one order. You bring the app, whether that is a chat, a voice room, a collaborative tool or a game; the network keeps everyone in step. This page installs the SDK, connects a room, and builds a chat in one file.
Install the SDK
The package on npm is arrr-network. It is an ES module, runs in Node.js 18 or newer and in any browser bundler, and has no runtime dependencies beyond ws for Node.
npm install arrr-networkimport { connect } from 'arrr-network';<!-- Defines the global arrrNetwork; arrrNetwork.connect is the same connect() -->
<script src="https://www.arrr.fun/sdk/arrr-network.iife.js"></script>The browser bundle above is the one this site serves. To host your own copy, clone arrr-sdk and run npm run build:browser; the file lands in dist/.
Get an app id and a key
On the public network a room belongs to an app. Sign up at the cloud console, create an app, and generate its API key; both go into connect(). Against a local service in open mode you can skip the key and use any appId. Details in API Keys.
Connect
const room = await connect('lobby', {
appId: 'app_…', // from the console
apiKey: 'arrr_…', // from the console; required on the public network
user: { id: 'alice' }, // who this client is; shows up in join/leave events
onTick(frame, inputs) { // every client gets the same batch, in the same order
for (const input of inputs) apply(input.data);
}
});
room.send({ move: { x: 1, y: 0 } }); // becomes someone's input.data next tickThe service picks a node for the room and the SDK connects to it. The default service is https://cloud.arrr.fun; pass centralServiceUrl to use your own.
Options you will use
| Option | What it does |
|---|---|
appId | The app the room belongs to. Required. |
apiKey | The app's key. Sent to the service, not to other players. Required when the service runs APP_REGISTRATION=key, which the public network does. |
user | An object with at least id. It is what other clients see in join, leave, disconnect and reconnect events, and what lets a dropped client reconnect as itself. |
centralServiceUrl | Where to ask for a node. Default https://cloud.arrr.fun. |
onConnect(snapshot, inputs, frame, node, fps, clientId) | You are in. snapshot and inputs are what a late joiner needs to catch up; fps is the room's tick rate, or 0 for a room with no clock, where a tick arrives only when someone sent something. |
onTick(frame, inputs) | The ordered batch for one tick. Each input has data (what someone sent), clientId, seq and frame. Games live here. |
onMessage(data, seq) | The same inputs, one at a time, without frames. Enough for chat and tools. |
onDisconnect() / onError(message) | The socket dropped, or the service refused you (a bad key is an onError). |
getStateHash() | Return a hash of your world each tick; the nodes compare hashes across clients and flag desync. |
sendSnapshot(snapshot, hash) / onSnapshot | Publish your world so late joiners start from it instead of replaying everything. |
sendVoice(x, y, z, data) / onVoice | Positional voice on the same socket, relayed to the nearest listeners and never into the simulation. |
The connection has send(data), leaveRoom() (a permanent exit, which others see as leave), close() (a drop, which others see as disconnect and you can reconnect from), plus connected and clientId.
Session events
Join and leave are inputs like any other, generated by the node so nobody can fake them. They arrive in onTick and onMessage with a type:
{ type: 'join', clientId: '…', user: { id: 'alice' } }
{ type: 'leave', clientId: '…', user: { id: 'alice' } } // called leaveRoom()
{ type: 'disconnect', clientId: '…', user: { id: 'alice' } } // socket dropped
{ type: 'reconnect', clientId: '…', user: { id: 'alice' } } // came back as the same userExample: a chat in one file
<!DOCTYPE html>
<body>
<div id="messages"></div>
<input id="text" placeholder="Type a message..." onkeypress="if(event.key==='Enter')send()">
<button onclick="send()">Send</button>
<script src="https://www.arrr.fun/sdk/arrr-network.iife.js"></script>
<script>
const me = 'guest-' + Math.random().toString(36).slice(2, 6);
let chat;
(async () => {
chat = await arrrNetwork.connect('lobby', {
appId: 'app_…', // your app, from the console
apiKey: 'arrr_…', // its key
user: { id: me },
onConnect() { log('Connected as ' + me); },
onMessage(data) {
if (data.type === 'join') log(data.user.id + ' joined');
else if (data.type === 'leave' || data.type === 'disconnect') log(data.user.id + ' left');
else if (data.text) log(data.from + ': ' + data.text);
},
onError(message) { log('Error: ' + message); }
});
})();
function send() {
if (!text.value.trim() || !chat) return;
chat.send({ from: me, text: text.value });
text.value = '';
}
function log(msg) {
const div = document.createElement('div');
div.textContent = msg; // never innerHTML with other people's text
messages.appendChild(div);
}
</script>
</body>Save it as index.html, serve it with npx serve ., and open it in two tabs. Everything each tab sends arrives in both, in the same order.
Running your own service
Against a local cloud service (npm run dev in arrr-cloud, then a node), pass centralServiceUrl: 'http://localhost:9001'. In its default open mode any appId works with no key.
From chat to a game
A game is the same connection with onTick instead of onMessage: run your simulation one step per tick, applying that tick's inputs first. Keep the simulation deterministic (same inputs, same result, on every machine), return a hash of the world from getStateHash, and publish snapshots with sendSnapshot so a player who joins late lands on the current frame. The live demos do exactly this; harness.js in the SDK repo's e2e fixtures is the tick loop to copy.
Next steps
- API Keys: register your app for the public network
- Live demos: a first-person shooter, a top-down shooter and a chat sharing state across tabs
- What is ARRR?: how the network orders inputs, and what it does and does not guarantee
- SDK on GitHub: source and the full option list
