Dedicated servers: how Steel Tide multiplayer works
Why the server owns the simulation, what a fog-filtered snapshot is, and what the one-line installer sets up on a Linux box.
Steel Tide 0.6 shipped multiplayer, and it took a shape that is a little unusual for an indie browser RTS: there is no host player. Every match runs on a dedicated server that simulates the world and the AI, and browsers are clients that send commands and draw what they are told. This post is about why, and what that means in practice.
Lockstep was not on the table
The classic RTS networking model is deterministic lockstep: every player runs the same simulation, only commands cross the wire, and everyone stays in sync because the simulation is bit-for-bit identical. It is elegant and cheap, and it demands that every floating-point operation, every iteration order and every random draw be identical on every machine. Steel Tide's simulation is seeded and repeatable in a single process, but it was never written to survive that contract across browsers, and retrofitting it would have meant rewriting the movement and combat code around a constraint that has nothing to do with gameplay.
The alternative is authority: one process runs the truth, and everyone else renders a copy. It costs bandwidth and it costs a server, but it also gives you things lockstep struggles with for free, like late joining, reconnection, and clients that cannot cheat by editing their own state.
Fog is enforced by the wire
The server steps the simulation at a fixed 30 Hz, the same rate as single player. Every third tick it builds a snapshot for each connected faction. That snapshot is not the world; it is the part of the world that faction is allowed to see. Enemy units inside your fog are simply not in the message, submarines you have no sonar on are not in the message, and a client that patched its renderer to draw everything would find nothing extra to draw. Snapshots are compressed by the WebSocket transport and clients interpolate between the two most recent positions, so a 10 Hz feed looks like a 60 Hz game.
Commands go the other way through the same validated boundary single player uses: every order is checked for ownership, target visibility, map bounds and known definition ids, and rate-limited per connection. An 8 MB cap covers uploaded saves.
Saves, checkpoints and coming back
The host can start a match from any local save, which means the lobby has to accept a file rather than a map name. Uploaded saves keep their map, entities, factions, campaign state and random number generator state; the server validates dimensions, faction counts and entity counts before it touches them. Once running, the server writes an atomic checkpoint at match start and every sixty simulated seconds. A browser that drops keeps an opaque reconnect token per server and reclaims its faction when it returns.
One command on a Linux box
None of this is useful if hosting is hard, so the server is a single Bun-compiled executable that also serves the complete web client. On a systemd Linux machine:
curl -fsSL https://rts.rene.wang/setup.sh | sudo sh
The installer creates an unprivileged service, generates a host key and a join code, and prints a host link and a player link. Add --domain play.example.com and it obtains a Let's Encrypt certificate, which matters more than it sounds: without TLS, a browser on an https page cannot open a plain WebSocket, so players have to be bounced to the server's bare IP and lose the saves stored under the origin they came from. The server guide has the details, including how to run it on port 443 or behind a reverse proxy.
What is not there yet
There is no matchmaking, no relay, no spectator mode and no chat. Direct-IP hosting for a group of friends is the whole product for now, and the design keeps that path open while the rest is built on top.