Host System
The Air Jam host system manages game sessions and input routing. This page explains how hosts work in different modes and how input flows through the system.
Host Modes
Standalone Host
The simplest mode—your game connects directly to the Air Jam server.
Usage:
import { AirJamHostRuntime, useAirJamHost } from "@air-jam/sdk";
const HostShell = () => (
<AirJamHostRuntime
input={{ schema: gameInputSchema }}
onPlayerJoin={(player) => spawnPlayer(player)}
onPlayerLeave={(id) => removePlayer(id)}
>
<HostView />
</AirJamHostRuntime>
);
const HostView = () => {
const host = useAirJamHost();
// Host receives all input directly
useFrame(() => {
host.players.forEach((p) => {
const input = host.getInput(p.id);
processInput(p.id, input);
});
});
return <GameCanvas />;
};Arcade Mode (Outer App + Embedded Game)
In arcade mode, the platform runs the outer Arcade app and your game runs as an embedded Air Jam host inside an iframe.
The important rule is:
- Arcade owns platform surface state
- your game owns gameplay state
- the server owns room membership, focus routing, and runtime invariants
Server-Authoritative Focus System
The server maintains authoritative control over which host receives controller inputs through a focus state:
Why Server-Authoritative?
- Security - Prevents rogue games from stealing input
- Reliability - Single source of truth for focus state
- Consistency - All controllers route to same host
This focus system does not replace the normal Air Jam model. It only decides which host currently receives controller input. Your game still uses the normal input lane, replicated state lane, and signal lane.
Connection Flow
1. Arcade Launch
[Platform] [Server] [Controller]
│ │ │
│ host:createRoom │ │
│ { maxPlayers: 8 } │ │
│ ───────────────────────────▶│ │
│ │ │
│ ack: { ok, roomId: "ABCD" } │ │
│ ◀───────────────────────────│ │
│ │ │
│ Display QR Code │ │
│ with room code │ Scan QR Code │
│ │ ◀──────────────────────────│
│ │ │
│ │ controller:join │
│ │ ◀──────────────────────────│
│ │ │
│ server:controllerJoined │ server:welcome │
│ ◀───────────────────────────│ ──────────────────────────▶│
2. Game Launch
[Arcade] [Server] [Your Game] [Controller]
│ │ │ │
│ system:launchGame │ │ │
│ ───────────────────▶│ │ │
│ │ │ │
│ ack: { launchCapability } │ │ │
│ ◀────────────────── │ │ │
│ │ │ │
│ Load iframe ───────────────────────────────▶│ │
│ │ │ │
│ │ host:joinAsChild │ │
│ │◀───────────────────── │ │
│ │ │ │
│ │ Focus → GAME │ │
│ │ │ │
│ │ Redirect controller │ │
│ │ ─────────────────────────────────────────▶│
│ │ │ │
│ │ │ Load game UI │
3. Active Gameplay
With focus set to GAME, all controller input routes to your game:
// Your game receives input normally inside the mounted host runtime.
const host = useAirJamHost();
useFrame(() => {
host.players.forEach((player) => {
const input = host.getInput(player.id);
// Process gameplay...
});
});4. Game Exit
[Your Game] [Server] [Arcade] [Controller]
│ │ │ │
│ host:exit │ │ │
│ ───────────────────▶│ │ │
│ │ │ │
│ │ Focus → SYSTEM │ │
│ │ │ │
│ │ server:gameEnded │ │
│ │ ─────────────────────▶│ │
│ │ │ │
│ Destroy iframe ◀────────────── │ │
│ │ │ │
│ │ Restore arcade UI │ │
│ │ ─────────────────────────────────────────▶│
Host Registration
Standalone Mode
<AirJamHostRuntime roomId="GAME" input={{ schema: gameInputSchema }}>
<HostView />
</AirJamHostRuntime>;
// Inside HostView:
const host = useAirJamHost();
// host.roomId contains the room code
// host.joinUrl contains full URL for QR codeProduction bootstrap remains automatic here:
- local dev: no auth ceremony
- static deploy: SDK bootstraps with your
appId - optional signed mode: SDK fetches a host grant from
hostGrantEndpointbefore bootstrap
Game code does not change between those modes.
Embedded Game Mode (Arcade)
// SDK auto-detects embedded runtime context from platform-injected params.
// Game code should not parse those params directly.
const host = useAirJamHost();
// Embedded close/lifecycle handling is owned by the arcade platform adapter.
// Game hosts should keep gameplay logic in host callbacks/input loops only.Player Management
Player Lifecycle
const HostShell = () => (
<AirJamHostRuntime
input={{ schema: gameInputSchema }}
onPlayerJoin={(player) => {
// player.id - Unique identifier
// player.label - Display name ("Player 1", etc.)
// player.color - Assigned color ("#FF5733")
// player.nickname - Optional custom name
spawnPlayerEntity(player);
announceJoin(player.label);
}}
onPlayerLeave={(controllerId) => {
removePlayerEntity(controllerId);
}}
>
<HostView />
</AirJamHostRuntime>
);Accessing Players
// Current player list
host.players.forEach((player) => {
console.log(player.label, player.color);
});
// Player count
const playerCount = host.players.length;
// Find specific player
const player = host.players.find((p) => p.id === targetId);Controller Presentation State
Use sendState for lightweight controller presentation metadata, not
authoritative gameplay sync. Networked stores should own the real multiplayer
state; this path is for controller-facing hints such as orientation, pause/play
status, and short messages.
// Update controller presentation
host.sendState({
runtimeState: "playing", // "playing" | "paused"
orientation: "landscape", // "portrait" | "landscape"
message: "Round 3 - Fight!",
});
// Controllers receive via onState callback
// or controller.runtimeState / controller.controllerOrientation / controller.stateMessageError Handling
const host = useAirJamHost();
// Check connection
if (host.connectionStatus === "disconnected") {
showReconnectUI();
}
// Check for errors
if (host.lastError) {
showError(host.lastError);
}
// Force reconnection
host.reconnect();