WebGL and Browser-Based Multiplayer Games

Everything a browser game needs is available without plugins, proxies or a server of your own — and the constraints that do exist are honest ones, documented here rather than discovered at launch.

What a browser client can do

A web page can call the complete REST surface: player registration and sessions, room create/join/leave, matchmaking, leaderboards, friends and invites, per-player JSON storage, server-side moderation and time sync. Every /api/*.php endpoint answers CORS preflights with Access-Control-Allow-Origin: * and whitelists the custom auth headers, so fetch() works directly — no CORS proxy, no JSONP, no extensions.

For live traffic the realtime relay speaks WebSocket, which every browser has supported for over a decade. The handshake accepts the realtime token as a query parameter precisely because browsers cannot set headers on a WebSocket upgrade — this is a deliberate server-side allowance for web clients, not a workaround.

const res = await fetch('https://api.michitai.com/api/game_players.php/heartbeat', {
  method: 'POST',
  headers: {
    'X-Api-Token': API_TOKEN,
    'X-Game-Player-Token': playerToken
  }
});
const body = await res.json();   // { success: true, ... }

The honest constraint: no UDP

Browsers cannot send raw UDP datagrams. The MUR1 fast lane that native clients use for movement-rate traffic (positions, inputs, analog state) is unavailable in a web page — there is no browser API for it. What happens instead: the server already delivers UDP-bound room traffic over WebSocket to peers that never bound a datagram address, and the JavaScript SDK's sendUnreliable() falls back to the WebSocket lane automatically.

Practically: a browser game gets one reliable ordered channel. For twitch gameplay at high update rates, batch state into fewer, smaller messages, or gate send rate. Turn-based, co-op, card, trivia, party and most .io-style games are comfortably inside what the WS lane handles — that's the design center for browser deployment.

The alternative browser datagram transport, WebTransport over HTTP/3, is evaluated on its own page — it requires server-side HTTP/3 infrastructure the current relay doesn't run, so it isn't offered today.

Using the JavaScript SDK

The JavaScript SDK is the same package browsers and Node.js share — plain ESM, zero dependencies, no build step. Import it from a bundler, an npm install, or copy src/ into a static project.

import { MultiplayerClient } from 'michitai-multiplayer';

const mp = new MultiplayerClient('API_TOKEN');        // public token only
const reg = await mp.players.register('WebPlayer');
const playerToken = reg.private_key;                  // persist via localStorage

const room = await mp.rooms.create(playerToken, 'Web Lobby', {
  maxPlayers: 8, realtime: true
});
const tok = await mp.realtime.get(playerToken);
const rt = mp.createRealtime({
  onReceive: (cmd, data, sender) => applyMessage(cmd, data, sender),
});
await rt.connect(tok.token, tok.realtime_server);     // wss://, query-token auth
rt.send('all', 'move', { x: 10, y: 4 });

Realtime tokens are short-lived (a 5-minute connect window) and single-connection — mint one per session through the SDK rather than hardcoding anything.

Unity WebGL builds

Unity's WebGL target compiles to wasm and restricts networking to what the browser offers: UnityWebRequest works (it maps to fetch), and .NET UdpClient/raw sockets do not — there is no socket API in a web page. For Unity WebGL, use the REST endpoints for lobby/room management (the Unity SDK's ?format=unity conventions apply), and the realtime channel through the browser's WebSocket — reachable from Unity via a JavaScript interop plugin or the JavaScript SDK alongside your build.

Other engines with HTML5 export (Godot 4 web builds, Construct, Defold, PlayCanvas) are in the same position: HTTP requests work, WebSocket works, UDP doesn't. Any engine that can make an HTTPS request can use this API — see Any Engine.

Security on a public client

Anything shipped to a browser is visible to its user. Ship only the public API token and per-player tokens — both are scoped to your game and grant no admin power. The private token (X-Api-Private-Token) must live exclusively on infrastructure you control; embedding it in a web bundle hands strangers ban/unban and game-data write access.

Player tokens are per-game and rotatable via /api/game_players.php/rotate; treat them like session cookies — store in localStorage or your own session layer, never in URLs or third-party analytics events.

Checklist for a browser launch

  1. Serve your game over HTTPS — mixed-content rules block fetch/wss from secure pages, and the realtime relay is TLS-only.
  2. Use the public API token only; keep the private token on your own backend if you need admin calls.
  3. Persist the player token (e.g. localStorage) so returning players keep their account.
  4. Design for one reliable channel: batch high-frequency updates, prefer sendUnreliable semantics for ephemeral state (it degrades to WS in-browser).
  5. Read Latency & Performance before promising ping numbers to players.