Infrastructure

Gamehoster's authoritative half is one box hosting many domains, each domain many games, each game many live instances. This page is how it serves them over the wire. Caddy is a static web server for each game's library and a reverse proxy for the rest; behind it one backend process runs an API web server, a websocket server that seats players, and a tick-and-send loop for every game it hosts. A game's own front end lives on its own website; the only things this box hands a browser are that one library file and a socket. The files all of this reads and writes are the Server page; the binary it speaks over the socket is the Protocol page.

Server architecture Caddy terminates TLS, serves each game's library straight from disk, and proxies /api and wss to one backend process. That process runs an API web server, a websocket server, and a tick-and-send loop for every game it hosts. Caddy · TLS — serves game libraries from disk, proxies /api and wss to the backend static /api wss STATIC · FROM DISK game library /<game>/gamehoster.client.js served by Caddy no backend hop BACKEND PROCESS · one process API web server /api · cert gate game sync + logs token-authed · bare 404 WebSocket server wss://…/<game> resolve · origin · seat Game loops per game: tick + send talks to its players direct
Caddy serves each game's library from disk and proxies /api and wss to one backend process, which runs the API web server, seats players, and runs every game's tick and send loops.

Static web server

The only file this server hands a browser is the game's custom library, and it is a plain static file. When a game is synced, the build writes a gamehoster.client.js into the game's folder: the client generated for that game, its config, schema tables and binary codec baked into the shared front-end code. Caddy serves it straight from disk, so no backend process is touched and it caches like any other asset. The game's front end is not served here; it lives on the game's own website and loads this one script to open the socket. There is no public index of games.

Interface — one static route per game, served by Caddy from the game's folder:

API web server

Hosts the API a Gamehoster box exposes: it takes each game's synced files and reads the box's logs. It is token-authed and returns a uniform bare 404 to anything unauthorised or unknown, so a probe with no token learns nothing about what exists; with no token configured, the API is off. Caddy proxies /api to it, and it answers Caddy's on-demand-TLS gate per hostname, so the box mints certs only for domains it hosts.

Interface — proxied to the backend:

The request-by-request detail lives on two pages: game synchronisation is the Sync page, and the access and server logs behind the Web Logs and Server Logs tools are the Tools page.

WebSocket server

Takes each player's game socket, authorises it, and seats the player into an instance. A game socket is one live player attached to one live instance for as long as they play, at wss://<domain>/<game>. A new socket is placed in three steps:

  1. The browser dials wss://<domain>/<game>; Caddy terminates TLS and proxies the upgrade to the backend on localhost.
  2. The server resolves (Host, first path segment) to a hosted game and checks the Origin against the game's allowed origins; an unknown game or a disallowed origin is dropped here.
  3. It seats the player into an instance with room, or a fresh one, sends the welcome, and from then on the update stream flows on that socket.

In dev, where the Host is localhost or a bare IP, the server falls back to the first game of that name on any domain, so one box serves every game by name without configuring DNS.

The tick and send loops

Each hosted game runs on two clocks. The tick clock advances every instance at the game's tick rate, running the three-step cycle from the tick: handle the tick's commands, update every actor, run the instance body. The send clock, usually slower, builds each player their update, the authoritative state of what they can see that changed since they last heard, and pushes it. The server is the only simulator: a command a player sends is queued and applied on the next tick of its instance, and no client runs a body.

Today one backend process carries all of this: the websocket server and every hosted game's loops share the one process, talking to each player directly over their socket. The Instance page is that mechanism in full.

Scaling across CPUs

One process is the whole of it today, and it is enough for a box hosting a modest set of games. The design scales the same model across CPU cores without changing it, and this part is not yet built.

It works because the instance is the unit of isolation: a complete little world with its own state, players, entities, tick number and short history, and nothing ever crosses between instances. So a game's instances can be dealt across a pool of worker processes, each a shard with a capacity cap. Two shards of one game then run truly parallel on two cores, and the core running one game is free of the others. A shard is spawned when load needs it and retired when its last instance empties, so idle costs nothing. Because nothing has to be coordinated across the split, any instance can run in any worker, which is what makes the pool safe to add under the single-process model already in place.

Layout and configuration

The hierarchy is a directory tree. Games live under one games root whose children are the domains, one directory per domain, named by the domain, each holding one directory per game, which carries that game's gamehoster-game.json, its synced files, and the generated gamehoster.client.js library. Names beginning _ or . are reserved for loose files and shared libraries. That one tree does double duty: Caddy serves the library files out of it, and the backend resolves each socket through the same <domain>/<game> path, a request's Host picking the domain directory and its first path segment picking the game. That tree, and every other file the box holds, is the Server page.

A few environment knobs place the backend: the port and bind address (localhost in production, behind Caddy; 0.0.0.0 in dev), the games root, and the file the sync token is read from (absent turns the API off). Each game then sets its own behaviour in gamehoster-game.json: the tick rate and send rate its two clocks run at, and the origins allowed to open its socket, a single origin or a list, where * or omitting the key allows any, *.example.com matches any subdomain, and a non-browser client sending no Origin is allowed through.