Node.js SDK and Server-Side Integration

The same michitai-multiplayer package doubles as the Node.js SDK — with two capabilities browsers can't have: the UDP fast lane and safe use of the private admin token.

Supported versions

The package declares "engines": {"node": ">=22.12"}. Recommended production target: Node 24 LTS (Active LTS since October 2025, supported through April 2028). Node 22 remains on Maintenance LTS until April 2027 and is fully supported. Node 26 (Current, promoted to LTS October 2026) is tested for forward compatibility. Node 20 is EOL and unsupported — it lacks the globals the SDK relies on.

Two engine features set the floor: the unflagged global WebSocket (realtime client without a ws dependency) and require() of ES modules (the CommonJS entry point). Both landed in the 22.x line. This follows the official release schedule at nodejs.org — recheck it when choosing a deployment image.

ESM and CommonJS entries

// ESM (both entry points)
import { MultiplayerClient } from 'michitai-multiplayer';
import { RealtimeClient } from 'michitai-multiplayer/realtime';

// CommonJS — works natively on Node >= 22.12 (require-of-ESM)
const { MultiplayerClient } = require('michitai-multiplayer');

There's no transpiled dual build — the ESM source is the package, and index.cjs re-exports it through Node's native ESM-require support. On older Node, await import('michitai-multiplayer') works from CJS files.

The UDP fast lane

After the WebSocket handshake, the server's connected envelope advertises udp_enabled and udp_port. connectUdp() dynamically imports node:dgram, sends a BIND datagram carrying the connection id issued over WS, and waits for BIND_ACK — the datagrams authenticate through the WS-issued id, so UDP alone can't hijack a session.

const rt = mp.createRealtime({ onReceive: handle });
await rt.connect(tok.token, tok.realtime_server);
await rt.connectUdp();                    // Node-only; rejects in browsers

// reliable ordered — WebSocket lane
rt.send('all', 'chat', { text: 'ready' });

// unreliable, unordered — UDP when bound, WS fallback otherwise
setInterval(() => rt.sendUnreliable('others', 'pos', { x, y }), 33);

Use the UDP lane only for loss-tolerant traffic — positions, analog input, ephemeral pings. Anything a missed packet would break belongs on send(). The wire format (MUR1) is documented in Realtime.

Server-only operations

Constructing the client with apiPrivateToken unlocks the admin surface: players.list(), players.ban()/unban(), and games.updateData(). Without it those methods throw ConfigurationError — a deliberate guard so a missing credential fails loudly instead of leaking a 401 to your users.

// server environment only — private token from env, never from the repo
const admin = new MultiplayerClient(process.env.MP_API_TOKEN, {
  apiPrivateToken: process.env.MP_PRIVATE_TOKEN,
});
await admin.players.ban(playerId, { banReason: 'cheating', banDuration: 3600 });

A typical production split: your game servers hold the private token and perform moderation/admin; game clients hold only the public token plus their own player token.

Timeouts, retries and production settings

  • timeout — per-request, default 30s; lower it for server-to-server calls (5–10s is sensible).
  • retries — GETs retry once on transport failure/5xx with backoff; POSTs never auto-retry (non-idempotent).
  • TransportError — thrown for network/timeout/non-JSON responses; carries .url and .cause for logs.
  • Keep one MultiplayerClient per process — it's stateless, and connection reuse in fetch keeps TLS handshakes off your hot path.
  • For headless tooling, the bundled mp CLI wraps all of this — CLI reference.