Browser multiplayer with Supabase: transport, state, and trust
A multiplayer prototype needs a way to exchange state, a rule for who owns that state, and a policy for what to trust. Those are different jobs. Supabase Realtime supplies the relay and presence features in Signal Strike; the browser game supplies its simulation and conflict rules.
The architecture is useful for a small shared demo. It is not a substitute for a server that validates competitive gameplay. Its limits are easiest to understand by following one update from sender to replica.
Put the transport behind a narrow interface
The current game3d.js has three adapters: Supabase broadcast and presence for internet rooms, BroadcastChannel for local same-origin tabs, and a scripted peer for offline checks. They expose join, send, and leave, with handlers for peers, messages, and connection state.
The Supabase adapter uses the channel prefix strike- and broadcasts game messages under event g. Presence tracks a participant’s id, display name, and join timestamp. The game logic consumes the resulting peer map rather than querying a database row for every frame.
Keeping the adapter small lets a local test exercise state transitions without contacting the live service. That test still cannot prove internet connection behavior, authorization, or production service limits.
An elected browser host owns shared simulation
Host selection orders participants by joinT, with id as a tie-breaker. The host advances shared systems such as bots and the storm. When a browser becomes host mid-match, the code adopts replicated state and resets parts of the bot simulation so it can continue.
let best = { id: NET.id, joinT: NET.joinT };
for (const peer of NET.peers.values()) {
if (peer.joinT < best.joinT ||
(peer.joinT === best.joinT && peer.id < best.id)) {
best = peer;
}
}
NET.hostId = best.id; This is the source’s selection rule. It is deterministic only for clients with the same participant data. Presence delivery is not instantaneous, and join timestamps originate on clients. The rule does not establish distributed consensus, synchronized clocks, or protection against a dishonest participant.
A production design needs to decide what happens during a network partition and who can legitimately declare a new match state. “The host left” is more complex than deleting a row when the old browser may still be running elsewhere.
Document the actual messages
Type field t | Purpose |
|---|---|
s | Player position x/y/z, yaw yw, pitch pt, health, and movement flags |
hit | Target id tgt, damage dmg, and shooter fields |
bs | Shared bot state in an array of rows |
st | Storm phase, radius, and center |
sc | Team scores and match state |
Names are part of the protocol. A guide showing type: "snap" with fields the application never sends is not a walkthrough of this code. If introducing a cleaner protocol for a tutorial, label it as a new example rather than silently substituting it.
Render slightly behind received state
The sender schedules player snapshots every 100 ms. A receiver stores the latest two valid positions and timestamps them with its own performance.now() when they arrive. It evaluates replicas at a local time 140 ms behind the present.
const renderTime = performance.now() - 140;
const fraction = (renderTime - previous.receivedAt) /
(current.receivedAt - previous.receivedAt);
const k = Math.max(0, Math.min(1.3, fraction));
const x = previous.x + (current.x - previous.x) * k; This renames the source fields for clarity and shows only one axis. Fractions from zero to one interpolate; values above one extrapolate. The current cap allows up to 30% of one received interval beyond the latest position. It then freezes rather than predicting indefinitely. Yaw takes the shorter path around the angle wrap.
Using local arrival timestamps avoids subtracting unsynchronized client clocks, but network jitter changes the apparent spacing. A deeper buffered timeline can handle more irregular delivery, at the cost of delay. No fixed interpolation window can make arbitrarily delayed packets look smooth.
A numeric guard is not authorization
The current player-snapshot handler rejects messages with non-finite position or yaw values. It does not reset the player to the origin. Damage is bounded to the interval from zero to 220. These checks protect pieces of simulation state, but they do not prove that a hit happened or that a claimed sender owns an identity.
The source still relies on a cooperative-client trust model. Some message handlers accept state without a cryptographically authenticated host check. The peer allocation guard is not a complete defense against every message shape. Do not describe the channel as secure merely because it has a short room code and a damage clamp.
For a less trusting environment, validate schemas and sender permissions at an authoritative boundary, use appropriate private-channel authorization, limit message rates, and verify gameplay-critical actions server-side. Private transport membership and authoritative gameplay solve different problems.
Do the fan-out arithmetic
For n senders each publishing r snapshots per second, outgoing snapshots total n × r per second. If every message reaches each other player, deliveries total n × (n - 1) × r. This excludes presence, bots, hits, scores, reconnects, and protocol overhead.
Four players at 10 Hz produce 40 outgoing snapshots per second, or 2,400 per minute. Their potential peer deliveries are 120 per second, or 7,200 per minute. These are message-count calculations, not the provider’s billing formula or a bandwidth estimate. Consult current service documentation and measure actual payload sizes before setting a capacity target.
Test isolation deliberately
Use same-origin tabs in the same storage partition for a BroadcastChannel test. Use separate browser contexts when an internet test needs independent account sessions. Those are different setups; isolating the contexts can prevent the local channel transport from communicating.
Check join, host selection, snapshots, host departure, reconnect behavior, malformed messages, and cleanup. Foreground scheduling matters for a real-time loop, but browser flags that suppress throttling change the environment. Record them. A local two-tab pass is evidence about that setup, not a claim that the internet architecture has no latency or trust problems.
Sources & revision
Read the dated source excerpt. It includes original file paths, line numbers, and SHA-256 hashes for the version used in this guide. The public repository may contain a different revision.
28 Oct 2026: Corrected wire message fields, host tie-breaking, interpolation timing, malformed-snapshot behavior, and message-rate arithmetic. Removed unsupported latency and security guarantees.
These notes use AI assistance. Read the editorial method and corrections policy. Found an error? Send a correction with this article’s title.