Configuration Reference

Complete reference for every configurable option in the CarverJS multiplayer system. All options have sensible defaults — you only need to override what you want to change.

tsx
import { useMultiplayer, useRoom } from "@carverjs/multiplayer";

useMultiplayer Options #

Pass these options to useMultiplayer() to control synchronization behaviour.

tsx
const multiplayer = useMultiplayer({
  mode: "prediction",
  tickRate: 60,
  broadcastRate: 20,
  keyframeInterval: 60,
  quantize: { position: 0.01 },
  prediction: { errorDecay: 0.85 },
  interpolation: { delay: 100 },
});

Top-Level Options #

OptionTypeDefaultDescription
mode'events' | 'snapshot' | 'prediction''snapshot'Sync strategy. events sends only inputs; snapshot sends full state; prediction adds client-side prediction and reconciliation
tickRatenumber60Fixed-timestep simulation rate in Hz. Higher = more precise physics but more CPU
broadcastRatenumber20How often the host sends state updates per second. Independent of tickRate
keyframeIntervalnumber60Number of ticks between full keyframe snapshots. Between keyframes, only deltas are sent
quantizeQuantizeOptionsReduce floating-point precision to save bandwidth
deltaThresholdsDeltaThresholdsMinimum change required before a field is included in a delta update
predictionPredictionSyncOptionsFull-world prediction and rollback settings (see below)
interpolationInterpolationOptionsHow remote entities are smoothed between updates
interestManagementInterestManagementOptionsArea-of-interest filtering for large worlds
debugDebugOptionsDebug overlay and network simulation tools
stepWorld() => voidSteps the physics world one fixed tick. Used for both forward simulation and rollback resimulation in prediction mode
onPhysicsStepPhysicsStepCallback(inputs, justPressed, tick, isRollback, dt) — simulation callback invoked once per fixed tick and once per resimulated rollback tick. Required for prediction mode

QuantizeOptions #

Quantization rounds values to a fixed step size, reducing the number of bits needed on the wire.

FieldTypeDefaultDescription
positionnumberStep size for position axes. 0.01 = centimetre precision
rotationnumberStep size for rotation (radians). 0.001 is typically sufficient
velocitynumberStep size for linear/angular velocity
tsx
quantize: {
  position: 0.01,   // ~1cm accuracy
  rotation: 0.001,  // ~0.06° accuracy
  velocity: 0.05,
}

DeltaThresholds #

Only include a field in the delta if it changed by more than the threshold since the last broadcast.

FieldTypeDefaultDescription
positionnumber0.001Minimum positional change (world units)
rotationnumber0.001Minimum rotational change (radians)
velocitynumber0.01Minimum velocity change
tsx
deltaThresholds: {
  position: 0.001,
  rotation: 0.001,
  velocity: 0.01,
}

PredictionSyncOptions #

Controls full-world prediction and rollback. Only relevant when mode is 'prediction'.

FieldTypeDefaultDescription
maxRewindTicksnumber15Max drift (ticks) between the local tick and serverTick + driftTargetTicks before the client hard-snaps its tick instead of resimulating
snapThresholdnumber150Per-axis position jump (world units) above which rollback visual correction is suppressed — intentional teleports stay instant
errorDecaynumber0.85Multiplicative decay applied to per-entity visual error offsets each render frame
maxErrorPerFramenumber0Maximum positional correction (units) applied per render frame. 0 disables the cap (the full decaying error is applied each frame)
neutralInputPlayerInput{}Fallback input payload used for unknown ticks or peers
inputHistorySizenumber120Tick-history ring size for local and per-peer inputs (about 2 seconds at 60 Hz)
driftTargetTicksnumber4Rollback snap target offset: snap target = serverTick + driftTargetTicks
tsx
prediction: {
  maxRewindTicks: 15,
  snapThreshold: 150,
  errorDecay: 0.85,
  maxErrorPerFrame: 0,
  neutralInput: {},
  inputHistorySize: 120,
  driftTargetTicks: 4,
}

InterpolationOptions #

Controls how remote (non-local) entities are smoothed between network updates.

FieldTypeDefaultDescription
delaynumber100Interpolation delay in milliseconds. Higher = smoother but more latency
maxExtrapolationnumber200Maximum time (ms) to extrapolate beyond the last received state before freezing
method'linear' | 'hermite''linear'Interpolation curve. Hermite produces smoother motion for accelerating objects
tsx
interpolation: {
  delay: 100,
  maxExtrapolation: 200,
  method: "hermite",
}

InterestManagementOptions #

Area-of-interest filtering. Only entities within range of the player are synchronized, dramatically reducing bandwidth in large worlds.

FieldTypeDefaultDescription
enabledbooleanfalseEnable spatial interest management
cellSizenumber50Spatial hash cell size in world units
viewDistancenumber200Maximum distance (world units) at which entities are synced to a client
hysteresisnumber20Buffer zone to prevent entities flickering in/out at the boundary
tsx
interestManagement: {
  enabled: true,
  cellSize: 50,
  viewDistance: 200,
  hysteresis: 20,
}

DebugOptions #

Developer tools for visualizing and simulating network conditions.

FieldTypeDefaultDescription
overlaybooleanfalseShow the on-screen debug overlay with live stats
simulatedLatencyMsnumber0Artificial one-way latency added to every message (ms)
simulatedPacketLossnumber0Fraction of packets to randomly drop (0–1). 0.05 = 5% loss
logLevel'none' | 'error' | 'warn' | 'info' | 'debug''warn'Console log verbosity
tsx
debug: {
  overlay: true,
  simulatedLatencyMs: 80,
  simulatedPacketLoss: 0.02,
  logLevel: "info",
}

useRoom Options #

Pass these options as the second argument to useRoom(roomId?, options?) — the first argument is the room to auto-join, or undefined to join later with room.join(id).

tsx
const room = useRoom("room-123", {
  displayName: "Player1",
  password: "secret",
  hostMigration: true,
  reconnectAttempts: 5,
  privacy: "relay",
});
OptionTypeDefaultDescription
transportCarverTransport--Pass a custom CarverTransport instance to bypass the built-in WebRTCTransport
passwordstring--Room password. Joining peers must provide the same value
displayNamestring--Human-readable name shown in the player list
playerMetadataRecord<string, unknown>--Arbitrary metadata attached to this player (avatar, team, skin, etc.)
iceServersRTCIceServer[]Provider defaultsCustom STUN/TURN servers for this room (overrides provider-level config)
hostMigrationbooleantrueAutomatically elect a new host when the current host disconnects
reconnectAttemptsnumber3Number of automatic reconnection attempts on disconnect
reconnectIntervalMsnumber2000Delay between reconnection attempts (ms)
privacy'all' | 'relay''all'Set to 'relay' to force all traffic through TURN servers (hides player IP addresses)
onConnected() => void--Callback when successfully connected to the room
onDisconnected(reason: string) => void--Callback when disconnected from the room
onHostMigration(newHostId: string) => void--Callback when the host changes
onError(error: CarverMultiplayerError) => void--Callback for any multiplayer error

Signaling Strategy Options #

Pass a strategy config to <MultiplayerProvider strategy={...}> to choose how peers discover each other. Defaults to { type: 'mqtt' } — free public brokers, zero configuration. The Firebase RTDB strategy takes the options below.

FirebaseStrategyConfig #

tsx
<MultiplayerProvider
  appId="my-game"
  strategy={{
    type: "firebase",
    databaseURL: "https://my-project.firebaseio.com",
    apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
    authTokenProvider: () => fetchCustomTokenFromYourBackend(),
    onAuthError: (error) => showReconnectPrompt(error),
  }}
>
FieldTypeDefaultDescription
type'firebase'Required. Selects the Firebase RTDB signaling strategy
databaseURLstringRequired. Realtime Database URL. Used to initialize a namespaced Firebase app unless firebaseApp is supplied
firebaseAppFirebaseAppAn already-configured Firebase app instance. Avoids double-init when your game uses Firebase elsewhere
authTokenProvider() => Promise<string>Returns a Firebase Auth custom token, minted by your own backend. Omit it and the strategy signals anonymously and never imports firebase/auth. Must return a fresh token on every call
apiKeystringFirebase Web API key. Required when authTokenProvider is set and no firebaseApp is supplied — Auth cannot sign in on an app initialized with databaseURL alone. A public project identifier, not a secret, but it must arrive through config and never be hardcoded in game source
onAuthError(error: Error) => voidCalled when authentication fails in a way the strategy cannot recover from on its own. See Authentication Errors

Signaling paths are written under ${appId}/__carver__/.... An appId containing a slash simply nests deeper, which is how you scope security rules per namespace.


Networked Config Prop Reference #

The config prop on <Networked> controls per-entity sync behaviour.

tsx
<Networked id="player-1" config={{ sync: "transform", owner: peerId, interpolate: true }}>
  <Actor type="primitive" shape="box" />
</Networked>
FieldTypeDefaultDescription
sync'transform' | 'rigid-body' | 'custom''transform'What data is synchronized. transform syncs position/rotation/scale; rigid-body adds velocity and angular velocity; custom sends only what you provide
ownerstringhost peer IDPeer ID that has authority over this entity. Only the owner can write state
customRecord<string, unknown>Arbitrary key-value pairs synced alongside transform. Useful for health, score, animation state
interpolatebooleantrueWhether remote copies of this entity use interpolation. Disable for instant-snap objects like UI cursors

Debug Tools #

DebugOverlay #

Enable the on-screen overlay to see live network statistics:

tsx
const multiplayer = useMultiplayer({
  debug: { overlay: true },
});

The overlay displays:

Toggle the overlay at runtime by pressing F3.

Network Simulator #

Inject artificial latency and packet loss for testing poor network conditions:

tsx
debug: {
  simulatedLatencyMs: 150,   // 150ms one-way delay
  simulatedPacketLoss: 0.05, // 5% random packet drop
}

Error Codes #

Errors delivered to useRoom's onError use CarverError with a code field from the CarverErrorCode enum. Firebase signaling authentication takes a separate path — see the uncoded row in the table and Authentication Errors below.

CodeMeaningCommon CauseRecovery
ROOM_NOT_FOUNDThe room ID does not existTypo in room ID, or room expiredVerify the room ID and retry
ROOM_FULLRoom has reached max playersAll player slots are takenShow "room full" UI, retry later
ROOM_LOCKEDRoom is locked by the hostHost called room.lock()Inform the user, wait for unlock
INVALID_PASSWORDWrong room passwordUser entered incorrect passwordPrompt for correct password
CONNECTION_FAILEDCould not establish a connectionFirewall, NAT, or network issue — also a Firebase custom-token sign-in that failed every retry, which rejects the joinRetry, or set privacy: 'relay' to force TURN
HOST_UNREACHABLECannot reach the room hostHost went offline without migrationWait for host migration, or rejoin
KICKEDKicked from the room by the hostHost called room.kick(peerId)Show "kicked" message to the user
SIGNALING_ERRORSignaling strategy errorMQTT broker or Firebase unreachableCheck network, retry after a delay
(no code)Firebase signaling authentication failureToken provider or sign-in failed, a write or listener came back permission_denied, or a listener was cancelled by the serverNot delivered as a CarverMultiplayerError — handle it in the strategy's onAuthError. See Authentication Errors
TURN_CREDENTIAL_ERRORTURN server credential failureExpired or invalid TURN credentialsRefresh credentials and retry
TRANSPORT_ERRORLow-level transport failureWebRTC data channel closed unexpectedlyAutomatic reconnection will attempt recovery
MIGRATION_FAILEDHost migration did not completeAll candidate hosts disconnected simultaneouslyRejoin or create a new room
tsx
const room = useRoom("room-123", {
  onError: (error) => {
    switch (error.code) {
      case "ROOM_FULL":
        showToast("Room is full. Try again later.");
        break;
      case "INVALID_PASSWORD":
        promptPassword();
        break;
      default:
        console.error(`[Carver] ${error.code}: ${error.message}`);
    }
  },
});

Authentication Errors #

Firebase signaling auth failures are reported through the strategy's own onAuthError callback, not as a CarverErrorCode. There is no auth code to switch on in useRoom's onError:

tsx
<MultiplayerProvider
  appId="my-game"
  strategy={{
    type: "firebase",
    databaseURL: "https://my-project.firebaseio.com",
    apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
    authTokenProvider: () => fetchCustomTokenFromYourBackend(),
    onAuthError: (error) => {
      console.error(error.message); // context + the RTDB error, kept as `cause`
      rejoinRoom();
    },
  }}
>

onAuthError receives a plain Error — not a CarverMultiplayerError — naming the operation that failed and carrying the underlying RTDB error as cause. It fires in three situations:

SituationStrategy behaviourWhat your app should do
The token provider or sign-in failed during init()The pair is retried 4 times in total (250ms, 1s, 4s backoff), then init() rejects. A failed init is not cached, so a later join retries from scratchSurface a connection error. The same failure also rejects the join, which useRoom reports as CONNECTION_FAILED
A re-auth cycle failedA permission_denied on the presence re-arm triggers exactly one re-auth cycle per connection. A further denial ends it; a new connection earns a fresh attemptTreat the room as unreachable for this player and rejoin
A signaling listener was cancelledOne re-auth cycle runs so writes can recover, but the listener is not re-subscribedRejoin the room — the only way to restore peer discovery

Type Definitions #

See Types for all type definitions including UseMultiplayerOptions, UseRoomOptions, NetworkedConfig, QuantizeOptions, PredictionSyncOptions, PlayerInput, PhysicsStepCallback, InterpolationOptions, InterestManagementOptions, DebugOptions, CarverError, and CarverErrorCode.