Architecture guide · TypeScript game server

Authoritative multiplayer in TypeScript with prediction and reconciliation.

Let clients send intentions, not trusted state. The server validates each input, advances the canonical simulation, and acknowledges processed sequence numbers while clients predict locally and replay unconfirmed inputs.

Presence, relay, and authority solve different problems.

Not every multiplayer feature needs an authoritative simulation. Use the least expensive model that preserves the integrity your game actually requires.

Presence

Share transforms and metadata so players see one another. Each client still owns its game.

Event relay

Broadcast social or gameplay facts through rooms without interpreting their payloads.

Per-player validation

Replay operations on a backend for anti-cheat saves while players remain independent.

Shared authority

One server owns contested world state, validates inputs, resolves conflicts, and sends snapshots.

Overworld's relay is intentionally opaque and is not an authoritative server. The net package supplies transport, presence, input channels, prediction, and reconciliation primitives. Your game supplies the simulation and server rules.

Put deterministic rules in a shared simulation function.

Prediction only converges when client and server apply the same input to the same state with the same elapsed time. Package that rule once and import it on both sides.

export type MoveInput = { dx: number; dz: number }
export type PlayerState = { x: number; z: number }

export function step(
  state: PlayerState,
  input: MoveInput,
  dtMs: number,
): PlayerState {
  const seconds = dtMs / 1000
  return {
    x: state.x + input.dx * 5 * seconds,
    z: state.z + input.dz * 5 * seconds,
  }
}

Do not read wall-clock time or call Math.random() inside the step. Pass time explicitly and use a seeded random source when simulation requires randomness. Keep side effects such as analytics, database writes, and socket sends outside the pure state transition.

Validate first, then advance the canonical state.

A client packet is untrusted. Verify its envelope, input shape, permissions, movement magnitude, rate, and time delta before applying it. Track the highest processed sequence number per player and include that acknowledgement with authoritative state.

socket.on('message', (raw) => {
  const envelope = JSON.parse(raw.toString())
  const message = envelope.data
  if (message?.t !== 'input') return

  const input = validateAndClamp(message.input)
  const dtMs = clamp(Number(message.dtMs), 0, 100)

  world.players[peerId] = step(
    world.players[peerId],
    input,
    dtMs,
  )
  lastSeq[peerId] = Math.max(lastSeq[peerId] ?? 0, message.seq)
})

setInterval(() => {
  for (const [peerId, socket] of sockets) {
    send(socket, {
      t: 'state',
      state: viewFor(peerId, world),
      lastSeq: lastSeq[peerId] ?? 0,
    })
  }
}, 50) // 20 Hz acknowledgement

The server may send a player-specific view instead of the entire world. Interest management, matchmaking, persistence, and fleet orchestration remain product infrastructure decisions rather than hidden behavior in the client library.

Predict immediately, then replay inputs after each acknowledgement.

Waiting a full round trip before showing local movement feels sluggish. Client prediction applies input immediately and records it with a monotonically increasing sequence number. When the server responds, the client resets to authoritative state and replays every input whose sequence is greater than the acknowledged value.

import {
  createInputChannel,
  createPredictedState,
  createWebSocketTransport,
} from '@overworld-engine/net'

const transport = createWebSocketTransport({
  url: 'wss://game.example/authority',
})
const channel = createInputChannel(transport)
const predicted = createPredictedState({
  initialState,
  step,
  maxPending: 128,
  onCorrection: (before, after) => {
    correctionBlend.start(before, after)
  },
})

channel.onServerState((state, lastSeq) =>
  predicted.onServerState(state, lastSeq)
)

const seq = predicted.applyInput(input, dtMs)
channel.sendInput(seq, input, dtMs)
render(predicted.state)

Ignore stale acknowledgements. Bound the pending-input queue. Use a domain-specific equality function with tolerances for floating-point state so harmless differences do not trigger visible corrections.

Predict the local player; interpolate remote snapshots.

Local prediction minimizes input latency. Remote entities have no local input stream to replay, so render them from a short snapshot buffer. Sampling slightly behind real time gives the client two states to interpolate between and absorbs network jitter.

  • Start near 1.5–2 times the snapshot interval for interpolation delay.
  • Use the shortest angular path for rotations and explicit interpolation for custom state.
  • Clamp at the newest snapshot when data stops instead of extrapolating forever.
  • Keep visual smoothing separate from the canonical simulation state.

Know what the framework does—and what your backend must do.

Overworld provides a stable JSON envelope, WebSocket transport, presence, rooms in the reference relay, input/state messages, predicted state, and a runnable authoritative server example. It does not claim to provide managed game-server fleets, databases, matchmaking, DDoS protection, or a universal conflict model.

If you need a complete hosted multiplayer backend, evaluate that category directly. If you already own the TypeScript simulation and want transport-neutral primitives that also work with React Three Fiber, Overworld keeps the client/server boundary explicit.

Keep rendering outside the server-safe core

Structure R3F as a presentation adapter over portable domain systems and authoritative snapshots.

Read the React Three Fiber architecture guide →