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.

Your Game(Host)AirJamHostRuntimeAir Jam ServerRoom: ABCDWebSocketInput EventsController 1(Phone)Controller 2(Phone)

Usage:

TSXsrc/components/HostView.tsx
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:

  1. Arcade owns platform surface state
  2. your game owns gameplay state
  3. the server owns room membership, focus routing, and runtime invariants
Air Jam Platform (TV)Arcade Browser (Outer App)• Owns the room• Handles game selection• Controls input focusYour Game (Embedded Host - iframe)• Joins via launch capability• Receives input when game has focus

Server-Authoritative Focus System

The server maintains authoritative control over which host receives controller inputs through a focus state:

FocusInput RecipientUse CaseSYSTEMOuter Arcade AppBrowsing games, navigationGAMEEmbedded Game HostActive gameplay

Why Server-Authoritative?

  1. Security - Prevents rogue games from stealing input
  2. Reliability - Single source of truth for focus state
  3. 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:

TSX
// 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

TSX
<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 code

Production bootstrap remains automatic here:

  1. local dev: no auth ceremony
  2. static deploy: SDK bootstraps with your appId
  3. optional signed mode: SDK fetches a host grant from hostGrantEndpoint before bootstrap

Game code does not change between those modes.

Embedded Game Mode (Arcade)

TSX
// 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

TSX
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

TSX
// 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.

TSX
// 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.stateMessage

Error Handling

TSX
const host = useAirJamHost();

// Check connection
if (host.connectionStatus === "disconnected") {
  showReconnectUI();
}

// Check for errors
if (host.lastError) {
  showError(host.lastError);
}

// Force reconnection
host.reconnect();