AURA

JSGG

AuraJS
DOCSEXAMPLESGITHUB
11 Networking and Multiplayer
Net sockets, discovery, multiplayer runtime expectations, and session-facing network behavior.
docs/external/game-dev-api/11-networking-and-multiplayer.md

Networking and Multiplayer

AuraJS currently exposes two different online surfaces:

  • aura.net for lower-level transport/HTTP-style work
  • aura.multiplayer for higher-level session/state/input replication

`aura.net` (optional module)

Enable with:

  • modules.network = true

Namespace methods:

  • connect(transport, host, port)
  • websocket(url)
  • fetch(url, options?)
  • get(url, options?)
  • post(url, bodyOrOptions?)

Connection objects returned by connect(...) and websocket(...) expose a callback-oriented surface with methods such as:

  • send(...)
  • onMessage(callback)
  • onDisconnect(callback)
  • close()

Practical notes:

  • onDisconnect listeners are intended to fire once per close transition.
  • Disabled-module calls fail with optional_module_net_disabled guidance.
  • The current runtime accepts ws:// websocket URLs. The source still has rough edges around wss://, so treat secure websocket support as something to verify against current host behavior before relying on it.

`aura.multiplayer` (optional module)

Enable with:

  • modules.multiplayer = true

Canonical flows

The simplified API is the recommended way to use multiplayer:

// Host: create a room. Auto room code, auto internet mode.
const room = aura.multiplayer.host({ maxPlayers: 4 });
// room.code = "ABCD" (auto-generated 4-char unambiguous code)

// Client: join by room code. Tries local, then internet automatically.
aura.multiplayer.join("ABCD");

When maxPlayers is set without an explicit internetMode, the runtime auto-infers internetMode: "auto" and resolves platform-default relay URLs. No manual URLs or room codes are needed for the common case.

In scaffolded projects, the same room-code model is exposed through the generated wrapper:

npm run dev
npm run join -- ABCD

Treat that wrapper as the default local-first dev path. The current local-multiplayer starter, also exposed as the multiplayer template alias, defaults to local-first hosting, but the same npm run dev + npm run join -- CODE flow upgrades to internet-backed hosting when you either:

  • set aura.config.json -> multiplayer.relay = "relay.aurajs.gg"
  • or launch with AURA_MULTIPLAYER_RELAY_HOST=relay.aurajs.gg

If a relay host or explicit relay URLs are present, the starter auto-promotes hosting to internet-backed auto mode. mode: "relay" is the advanced opt-in when you want to skip direct UDP and force relay transport.

If you also set aura.config.json -> multiplayer.launcherBaseUrl, the starter can surface a web join page like https://auralauncher.gg/join/AURA2P while keeping the same npm run dev + npm run join -- CODE runtime flow.

If you want the smallest copyable example with host and join in one project, use examples/multiplayer-party. It keeps the same room-code model and adds diagnostics plus room chat.

Current state/read timeline truth

AuraJS now has three multiplayer read paths on purpose:

  • getState() / getAllState() are the raw authoritative replicated snapshot
  • getInterpolatedState() / getInterpolatedAllState() are additive client render helpers
  • getRollbackState() / getRollbackAllState() are rollback-aware gameplay reads for configured supported lanes

Use the raw state when you need canonical replicated truth. Use the interpolated helpers when a client wants smoother rendering. Use the rollback helpers when the client has explicitly configured a supported rollback lane. On the host, the additive helper reads resolve back to authoritative state.

onStateUpdate(function (snapshot, metadata) { ... }) now receives additive metadata with:

  • sequence
  • serverTick
  • serverTimeMs
  • tickIntervalMs
  • inputAckTicks
  • source
  • jitterMs
  • bufferDelayMs
  • historyDepth
  • bufferedServerTimeMs

source: "transfer" marks a continuity boundary such as migration/state transfer and should snap rather than smear.

Current truthful limit:

  • configureRollbackLane, getRollbackState, getRollbackAllState, clearRollbackLane, and getRollbackDiagnostics are now the shipped supported rollback lane for runtime-owned gameplay state.
  • configureLocalPrediction, setLocalPredictedState, and getLocalPredictionDiagnostics remain available as the lighter Stage 173 overlay lane for authored opt-in overlays that do not use the supported rollback lane.
  • getRoomInfo().transportStatus and getNetworkDiagnostics() now surface reason-coded recovery states such as resume_pending, resumed, and explicit fallback/error transitions.
  • configureNetworkImpairment() / clearNetworkImpairment() provide deterministic latency, jitter, loss, and short-outage hooks for tests.
  • AuraJS still does not ship arbitrary authored-gameplay rollback or universal internet rescue.
  • Stage 174 freezes the current recovery and supported rollback boundary in ../../multiplayer-internet-hardening-and-rollback-grade-feel-contract-v1.md.

TL;DR: hook multiplayer into an existing game

Minimal host-authoritative loop:

aura.multiplayer.configure({ maxPlayers: 2, tickRate: 20 });

function startHost() {
  return aura.multiplayer.host({
    roomCode: "AURA2P",
    maxPlayers: 2,
    internetMode: "local", // switch to "auto" or "relay" for internet-backed rooms
  });
}

function hostTick(localInput) {
  aura.multiplayer.setState("player_0", stepPlayer(aura.multiplayer.getState("player_0"), localInput));
  const inputs = aura.multiplayer.getAllPlayerInputs() || {};
  for (const [playerId, input] of Object.entries(inputs)) {
    const key = `player_${playerId}`;
    aura.multiplayer.setState(key, stepPlayer(aura.multiplayer.getState(key), input));
  }
}

function clientTick(input) {
  if (aura.multiplayer.isConnected()) {
    aura.multiplayer.sendInput(input);
  }
}

function draw() {
  const allPlayers = aura.multiplayer.isHost()
    ? (aura.multiplayer.getAllState() || {})
    : (aura.multiplayer.getInterpolatedAllState()
        || aura.multiplayer.getAllState()
        || {});
  // draw shared state here
}

TL;DR:

  • call configure(...) once
  • host with host({ roomCode, maxPlayers, internetMode })
  • clients use npm run join -- CODE or aura.multiplayer.join("CODE")
  • host reads getAllPlayerInputs() and publishes authoritative state with setState(...)
  • host logic reads raw getState() / getAllState()
  • clients that want smoother motion should prefer getInterpolatedState() / getInterpolatedAllState() for draw

Starter TL;DR:

auramaxx create my-room-game --template multiplayer
cd my-room-game
npm run dev

# second terminal
npm run join -- AURA2P

Fastest internet-backed upgrade for that starter:

{
  "multiplayer": {
    "relay": "relay.aurajs.gg",
    "launcherBaseUrl": "https://auralauncher.gg"
  }
}

Or keep the project untouched and launch with:

AURA_MULTIPLAYER_RELAY_HOST=relay.aurajs.gg npm run dev
AURA_MULTIPLAYER_RELAY_HOST=relay.aurajs.gg npm run join -- AURA2P

If launcherBaseUrl is configured, the starter HUD can also show a shareable join page:

https://auralauncher.gg/join/AURA2P

Auto room code generation

Room codes are 4-character strings from an unambiguous alphabet (A-Z excluding O/I/L, 2-9 excluding 0/1). If a specific code is needed, pass roomCode explicitly:

const room = aura.multiplayer.host({ maxPlayers: 4, roomCode: "MYROOM" });

Default relay URLs and overrides

Platform defaults:

  • Coordinator: tcp://relay.auramaxx.com:43110
  • Relay: tcp://relay.auramaxx.com:43111

Override with environment variables:

  • AURA_COORDINATOR_URL=tcp://myhost:43110
  • AURA_RELAY_URL=tcp://myhost:43111

Override with explicit options:

aura.multiplayer.host({
  maxPlayers: 4,
  coordinatorUrl: "tcp://myhost:43110",
  relayUrl: "tcp://myhost:43111",
});

Relay shorthand expands a hostname to both ports:

aura.multiplayer.host({ maxPlayers: 4, relay: "mygame.example.com" });
// Equivalent to coordinatorUrl: "tcp://mygame.example.com:43110",
//               relayUrl: "tcp://mygame.example.com:43111"

Precedence: explicit option > relay shorthand > env var > platform default.

Internet-fallback join

join("CODE") with no internet options follows this resolution order:

  1. Look up the room code in the local filesystem room registry.
  2. If not found locally, fall back to the default coordinator for internet room lookup.

getRoomInfo().joinPath reports how the room was resolved:

  • "local" -- found via local registry
  • "internet_fallback" -- local lookup failed, found via coordinator
  • "internet_explicit" -- developer specified internet options
  • "direct" -- developer used join(host, port) direct-connect path

Session/config/state/input methods

  • configure(options) -- set maxPlayers (2--16), tickRate, protocol
    • protocol: "tcp" keeps reliable state/messages/input on the reliable lane
    • protocol: "udp" / "both" only switch local/direct gameplay input to UDP after room metadata reports transportStatus: "udp_input_ready"
  • host(portOrOptions) -- create a room (returns room metadata object)
  • join(codeOrHost, portOrOptions?) -- join a room by code or direct address
  • leave() -- disconnect from the session (client)
  • stop() -- stop hosting and disconnect all clients (host)
  • isHost() / isClient() / isConnected()
  • getLocalId() -- this player's unique ID (0 = host)
  • getPlayers() -- [{ id, name, rtt, connected, isHost, transport }]
  • getPlayerCount() -- number of connected players including host
  • getPing() -- local player's RTT in ms (0 for host)
  • getServerTime() -- synchronized clock (ms since session start)
  • getRoomInfo() -- current room/session metadata (see below)
  • isMigrating() -- true during host migration grace period
  • getInviteCode() -- current room code string or null
  • getInviteLink() -- "aurajs://join/CODE" URI or null
  • copyInviteLink() -- copy invite URI to clipboard; returns boolean
  • send(targetId, type, payload) -- send a message to one player
  • broadcast(type, payload) -- send a message to all players
  • sendInput(payload) -- client sends input snapshot to host
  • getPlayerInput(playerId) -- host reads a player's latest input
  • getAllPlayerInputs() -- host reads all player inputs
  • setState(key, value) -- host sets replicated state
  • getState(key) -- read replicated state
  • getAllState() -- read full state snapshot
  • getInterpolatedState(key) -- read additive smoothed state for one key
  • getInterpolatedAllState() -- read additive smoothed full state snapshot
  • configureRollbackLane(options) -- enable a supported rollback/prediction/replay lane
  • getRollbackState(key) -- read rollback-aware state for one key
  • getRollbackAllState() -- read rollback-aware full state snapshot
  • clearRollbackLane(key?) -- clear one rollback lane or all lanes
  • getRollbackDiagnostics(key?) -- inspect rollback/replay history and corrections

Callback registration methods

  • onMessage(type, callback) -- receive game messages
  • onStateUpdate(callback) -- called on clients when state arrives as (snapshot, metadata)
  • onPlayerJoin(callback) -- { id, name }
  • onPlayerLeave(callback) -- { id, name, reason }
  • onDisconnect(callback) -- called on client when connection lost
  • onHostMigration(callback) -- { newHostId, previousHostId, isNewHost }
  • onHostChanged(callback) -- { newHostId, previousHostId }

Player/room metadata helpers

  • kick(playerId, reason?) -- host kicks a player
  • setPlayerData(playerId, key, value) / getPlayerData(playerId, key)
  • advertise(options) -- LAN discovery broadcast
  • discover(discoveryPort, callback) -- scan for LAN hosts

getRoomInfo()

Returns truthful room/session metadata:

{
  role: "host" | "client",
  code: "ABCD",
  scope: "local" | "direct" | "internet",
  transportPath: "local_room" | "direct_tcp" | "direct_udp_pending" | "direct_udp" | "relay_pending" | "relay_room",
  transportStatus: null | "registered" | "relay_ready" | "direct_udp_pending" | "direct_udp_ready" | "direct_udp_timeout" | "relay_fallback" | "input_udp_pending" | "udp_input_ready" | "tcp_input_fallback" | "error",
  lastReasonCode: null | "direct_udp_timeout" | "direct_runtime_attach_failed" | "relay_runtime_attach_failed" | "room_expired" | "relay_attach_failed" | "control_heartbeat_timeout" | "peer_left" | "host_migrating" | "input_udp_bind_failed" | "input_udp_connect_failed" | "input_udp_address_resolution_failed" | "input_udp_io_error" | "input_udp_not_ready" | "input_protocol_fallback_tcp",
  peerCandidate: { host: "203.0.113.10", port: 54321 } | null,
  requestedMode: "local" | "relay" | "auto" | "p2p",
  joinPath: "local" | "internet_fallback" | "internet_explicit" | "direct" | null,
  migrating: boolean,
  migrationCompleted: boolean,
  previousHostId: number | null,
  newHostId: number | null,
  connectivity: {
    mode: "local" | "relay" | "auto" | "p2p",
    coordinatorUrl: "tcp://relay.auramaxx.com:43110" | null,
    relayUrl: "tcp://relay.auramaxx.com:43111" | null,
  },
}

Direct-address sessions report scope: "direct" and transportPath: "direct_tcp". For local/direct TCP-only sessions, transportStatus may stay null until there is a real setup or fallback state to surface.

During Stage 174 recovery, client room metadata may temporarily report transportStatus: "resume_pending" and then transportStatus: "resumed" on success. Failed recovery leaves an explicit lastReasonCode and transitions to terminal cleanup instead of pretending the session never degraded.

Host Migration

Host migration is opt-in and only works for relay-backed sessions. When enabled, if the host disconnects, the relay elects the connected client with the lowest peer_id as the new host. State is preserved from each client's cached replication snapshot.

Truthful limit:

  • this is continuity for replicated authoritative state
  • Stage 174 recovery keeps buffered, predicted, and rollback state reset or re-armed explicitly across continuity boundaries
  • it does not imply arbitrary authored-game rollback or seamless recovery for unsupported client-local state

Enable host migration:

aura.multiplayer.configure({
  hostMigration: true,
  migrationGraceMs: 3000,  // grace period in ms (default 3000)
});

Both host and clients should call configure() before host() / join().

Callbacks:

  • onHostMigration(callback) -- fires when migration starts.
    aura.multiplayer.onHostMigration(function (info) {
      // info: { newHostId, previousHostId, isNewHost }
      // isNewHost is true if this peer is the elected new host.
    });
    
  • onHostChanged(callback) -- fires when migration completes.
    aura.multiplayer.onHostChanged(function (info) {
      // info: { newHostId, previousHostId }
    });
    

Query methods:

  • isMigrating() -- returns true during the migration grace period. During migration, update() is paused but draw() still runs, so the game can display a "Migrating..." overlay.

getRoomInfo() migration fields:

{
  migrating: true | false,
  migrationCompleted: true | false,
  previousHostId: 0 | null,
  newHostId: 2 | null,
}

Social Invites

Convenience helpers for sharing room codes:

  • getInviteCode() -- returns the current room code string, or null.
  • getInviteLink() -- returns an aurajs://join/CODE URI string, or null.
  • copyInviteLink() -- copies the invite URI to the system clipboard via SDL2. Returns true on success, false if no session or clipboard unavailable.
var link = aura.multiplayer.getInviteLink();
// "aurajs://join/ABCD"

aura.multiplayer.copyInviteLink();
// Copies "aurajs://join/ABCD" to clipboard.

--join CLI flag:

Players can join a room directly from the command line:

auramaxx run . --join ABCD

This auto-joins the specified room after setup() completes. It is equivalent to calling aura.multiplayer.join("ABCD") in game code.

In a scaffolded game, npm run join -- ABCD delegates to the same join flow through the generated wrapper.

Per-Player Transport

getPlayers() returns a transport field for each player indicating how they are connected:

var players = aura.multiplayer.getPlayers();
// [{ id: 0, connected: true, isHost: true, rtt: 0, ping: 0, transport: "local" },
//  { id: 1, connected: true, isHost: false, rtt: 12, ping: 12, transport: "direct_udp" },
//  { id: 2, connected: true, isHost: false, rtt: 45, ping: 45, transport: "relay" }]

Transport values:

  • "direct_udp" -- connected via direct UDP (NAT punch succeeded)
  • "relay" -- connected via relay
  • "direct_tcp" -- connected via direct TCP
  • "local" -- local player (host self-reference)

In a single session, different players may have different transport types (mixed transport). The host handles both direct and relay traffic transparently.

Practical notes

  • Disabled-module calls fail with optional_module_multiplayer_disabled guidance.
  • The runtime is callback-oriented and deterministic. Host/client callbacks are scheduled as part of the main runtime loop.
  • N-player relay rooms support up to 16 concurrent players per room.
  • internetMode: 'auto' and 'p2p' try direct UDP gameplay first and fall back to relay when the direct attempt times out.
  • Relay/runtime lifecycle reasons such as room_expired, relay_attach_failed, direct_runtime_attach_failed, relay_runtime_attach_failed, and control_heartbeat_timeout surface through getRoomInfo() after expiry or attach failure.
  • Prefer room-code join as the default model.
  • The direct join(host, port) path still exists for advanced use cases.
  • Treat explicit internetMode, relay URLs, and advertise() / discover() as advanced or operator-facing flows rather than the default starter story.
  • The shipped internet path uses the standalone @aurajs/multiplayer-relay package in the monorepo as coordinator plus fallback transport.
  • Host migration is opt-in and relay-only. Enable with configure({ hostMigration: true }) before host() or join().
  • Social invite helpers (getInviteCode, getInviteLink, copyInviteLink) require an active session. copyInviteLink requires SDL2 clipboard access.
  • The --join CODE CLI flag auto-joins a room after setup without modifying game code.
  • Public relay hosting, TURN, and general internet discovery are still future work. The default relay URL (relay.auramaxx.com) is a placeholder for when the public fleet launches; developers must run their own relay for now.

Use aura.net when you need:

  • raw transport connections
  • websocket or HTTP-like integrations
  • non-gameplay service calls

Use aura.multiplayer when you need:

  • room/session creation with automatic room codes
  • N-player relay rooms (up to 16 players)
  • player lists and presence (with per-player transport info)
  • replicated state and player input
  • internet-fallback room resolution (local then coordinator)
  • host migration (opt-in, relay-only)
  • social invite helpers (invite codes, links, clipboard)
  • LAN advertising/discovery
DOCUMENT REFERENCE
docs/external/game-dev-api/11-networking-and-multiplayer.md
AURAJS
Cmd/Ctrl+K
aurajsgg