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.
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:
GET https://<domain>/<game>/gamehoster.client.js— the game's custom library, written at sync, served static
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:
/api/…— the game-definition sync surface and the read-only logs and stats, both token-authedGET /internal/allow— Caddy's cert gate:200only for a domain the box hosts
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:
- The browser dials
wss://<domain>/<game>; Caddy terminates TLS and proxies the upgrade to the backend on localhost. - The server resolves
(Host, first path segment)to a hosted game and checks theOriginagainst the game's allowed origins; an unknown game or a disallowed origin is dropped here. - 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.