Server

Two processes run on the box: Caddy, the only public listener, and a small Node game server bound to 127.0.0.1:8788 that Caddy reverse-proxies to. The game server loads each game's generated module and runs its tick, serves the game's static library, answers the game sockets, and carries the token-authed sync and tools API (it also samples the machine's health on a timer). There is no configuration to speak of — every path is fixed in the code or a systemd environment line, and the only per-install state is one secret. Everything Gamehoster puts on disk lives under three roots. How it all serves in the large — the target split into a static server, a websocket server and worker processes — is the Networking page.

/opt/gamehoster/
  server/                         the server code (+ the ws dependency)
  common/                         the shared tools toolkit
  shared/                         the health snapshot + stats recorder
  games/                          the games mirror — one folder per domain
  token                           the bearer token (the one secret)
  VERSION                         the installed bundle's build stamp
/srv/
  gamehoster-logs/                Caddy's access logs
  gamehoster-stats/               server-health samples
/etc/
  gamehoster/                     the self-signed bare-IP cert
  caddy/
    Caddyfile                    the Caddy config
  systemd/
    system/                      the service units
The files and directories on the server.

Contents

Every path is fixed in the code or the service unit; the installer creates all of them and nothing lives outside them.

NameTypeDescription
/opt/gamehoster/games/directoryThe games mirror — one folder per domain, each holding game folders — what the sync writes and the server loads and serves.
/opt/gamehoster/server/directoryThe program itself: every source file the server runs, plus its one dependency, ws.
/opt/gamehoster/common/ · shared/directoryThe shared tools toolkit and the stats recorder the server requires, copied verbatim across every hoster.
/opt/gamehoster/tokenfileThe one secret: the bearer token the whole API is gated on. Absent turns the API off.
/srv/gamehoster-logs/directoryCaddy's combined JSON access log, active plus size-rotated files.
/srv/gamehoster-stats/directoryThe server-health samples, one dated file per statistic type.
/etc/gamehoster/directoryThe self-signed certificate for the bare-IP endpoint.
/etc/caddy/CaddyfilefileThe Caddy config the installer lays down, plus a backup of any earlier one.
/etc/systemd/system/directoryThe service units — the gamehoster server, and Caddy's from its package.

/opt/gamehoster/games/

The games mirror — the tree the sync pushes up and the server loads and serves. Its children are the domains: one directory per domain, named exactly for the host, each holding one directory per game. A game folder carries its gamehoster-game.json config and definition files, plus the two generated files the sync produces — the static server module the server requires, and the library the browser loads. Names beginning _ or . are reserved: the sync never pushes or prunes them, so the box can hold a _shared/ library the client does not own. It is owned by the gamehoster service user, the only tree the server writes.

/opt/gamehoster/games/
  game.example.com/                a DOMAIN — a dotted directory, named for the host
    asteroids/                     a GAME
      gamehoster-game.json         the game config (tick/send rates, capacity, origins)
      gamehoster.server.js         GENERATED — the static module the server requires
      gamehoster.client.js         GENERATED — the library handed to the browser
      gamehoster-player/            player schema + join / leave / view / update bodies
      gamehoster-instance/          instance schema + update bodies
      gamehoster-entities/          one folder per entity type: schema + update bodies
      gamehoster-commands/          one folder per command: schema + writes + handler
  _shared/                         reserved (_ prefix) — never synced, never pruned
The games mirror: one folder per domain, each holding game folders.

The server resolves each socket through the same <domain>/<game> path — the request's Host picks the domain, the first path segment picks the game — and serves that game's gamehoster.js library from its folder. A file arriving on the API is written to its path here, and a short debounce folds a burst of writes into a single reload; a domain that hosts no game with a valid gamehoster-game.json is simply skipped. The structure rules that decide what a domain and a game are live on the Sync page.

/opt/gamehoster/server/

The program installs under a fixed directory; the service unit points straight at server/server.js. The tree below is every source file the running server loads — the entry point and the modules it imports — plus the sibling common/ and shared/ trees it requires, copied verbatim across the hoster family. Every module is plain Node; the one dependency is ws, the pure-JS WebSocket library, bundled into node_modules so the box needs no npm install. The generate.js that produces the generated files is not here — generation runs on the client at sync, and the server only ever requires what it receives.

/opt/gamehoster/
  server/
    server.js                      the entry systemd runs (HTTP + WebSocket + API)
    engine.js                      the World: instances, the tick, rollback, deltas
    loader.js                      load a game dir into a runnable game
    schema.js                      pure schema helpers (shared with the browser)
    package.json                   declares the one dependency, ws
    node_modules/
      ws/                          the pure-JS WebSocket library, bundled
  common/
    server/
      logs-stats.js                the read-only /logs + /stats routes
  shared/
    stats.js                       one machine-health snapshot
    stats-recorder.js              the loop that writes the stat day-files
Every source file the server runs, and its one bundled dependency.
NameTypeDescription
server/server.jsfileThe server and the process systemd starts. One localhost HTTP listener carrying the game sockets (wss://<domain>/<game>), the static library route, the sync surface, the read-only logs/stats routes, the version probe, and Caddy's on-demand-TLS gate. Loads every game on start, reads the token once, and starts the stats recorder on listen().
server/engine.jsfileThe authoritative engine: one World per game holds its live instances and players and advances them one tick at a time — commands, per-actor updates, the instance body — with snapshot/rewind for late commands and the per-player delta protocol. The same engine is bundled into the browser for prediction. The Instance page is this in full.
server/loader.jsfileLoads a game directory into a runnable game. It prefers the generated gamehoster.server.js (a plain require, no new Function); a dev-only fallback interprets the raw definition when no generated module is present.
server/schema.jsfilePure schema helpers with no Node dependencies — read a state schema to field descriptors, compute defaults, decide which fields a front-end body may write — so the server and the bundled browser engine compute identically.
server/package.json · node_modules/ws/file · dirThe one dependency, ws, and its lockfile, bundled so the target box needs only Node and Caddy.
common/server/logs-stats.jsfileThe read-only /logs and /stats routes (plus the /stats/now debug snapshot), identical across every hoster and mounted under the same token.
shared/stats.jsfileTakes one snapshot of the machine's health — CPU, memory, disk, and cumulative disk- and network-IO counters. Common to every hoster.
shared/stats-recorder.jsfileStarted by the server, it snapshots on the interval, appends a JSON line per type to the dated day-file, and prunes files past the retention window.

Also copied under the version dir but not run by the server: the rest of common/ (net.js, browse/, client/) — the shared client and tools toolkit, carried so one bundle serves every role.

/opt/gamehoster/token

The whole per-install configuration is one secret. The installer generates it once (32 random bytes, printed once) and the server reads it at startup; the sync and tools client is handed the same value out of band. Every API call is gated on it, and with no token file the API is off entirely — the sockets still serve, but nothing can be pushed or read. To rotate it, write a new value and restart the server. It is mode 600, owned by the gamehoster user. Alongside it, a VERSION file records the installed bundle's build stamp.

/srv/gamehoster-logs/

Caddy writes one combined JSON access log covering every game domain and the IP endpoint, each line tagged with its host. It rotates by size only (there is no daily rotation), so there is one active file and a trail of size-rotated ones. The server exposes this directory read-only (GET /api/v1/logs) and the client mirrors it down for the Web Logs tool; the directory is mode 0770 group caddy, which writes it, and the gamehoster user is added to the caddy group so the server can read it.

/srv/gamehoster-logs/
  access.log                    the active log, one JSON line per request
  access-1784514159284.log      rotated and immutable; the number is its rotation time in ms
  access-1784370489663.log
Caddy's active and rotated access logs.

Rotation is set in the Caddyfile (roll_size 64MiB, roll_keep 50, roll_keep_for 2160h). Rotated files are keyed to the client by mtime, never by name, so filenames never cross the wire.

/srv/gamehoster-stats/

The server samples the machine's own health every 15 seconds and appends one JSON line per statistic type to a dated, per-type day-file. A day-file is frozen the moment the UTC date rolls over — no rename, no lock — and files older than 60 days are pruned. The server serves these read-only (GET /api/v1/stats); the client mirrors each past day once and keeps it, which is the data behind the Server Logs tool. The directory is mode 750, owned by the gamehoster user, and is the only path besides games/ the service may write.

/srv/gamehoster-stats/
  cpu-2026-07-20.log              per-core cumulative CPU times
  memory-2026-07-20.log           total / used / available / free bytes
  disk-2026-07-20.log             usage of the filesystem holding /
  diskio-2026-07-20.log           cumulative bytes read / written
  netio-2026-07-20.log            cumulative bytes in / out
  cpu-2026-07-19.log              yesterday's files, immutable until the retention cutoff
One dated file per statistic, per day.

Rates are never stored: the counter fields are cumulative, so the reader diffs two samples and divides by the true elapsed time — a gap after downtime averages out instead of spiking, and a reboot's counter reset is dropped. Metrics a host can't report (e.g. disk/net IO off Linux) are simply absent.

/etc/gamehoster/

A self-signed certificate for the bare-IP endpoint, generated once by the installer (CN=gamehoster, ten-year life). Caddy obtains real certificates on demand for the game domains; this one only completes the handshake for a no-SNI / bare-IP connection, selected via the Caddyfile's default_sni. It is what lets the sync client reach the API by the server's IP — the client skips verification for an IP endpoint, and the token is what actually authenticates it.

/etc/gamehoster/
  tls.crt                        self-signed cert for the bare-IP endpoint
  tls.key                        its private key (mode 640 root:caddy)
The bare-IP certificate.

/etc/caddy/Caddyfile

The Caddyfile is fully static — no environment placeholders. It terminates TLS, obtains real certs on demand (gated by a localhost call to /internal/allow, so issuance can't be forced for domains you don't host), redirects HTTP → HTTPS, serves the bare-IP block from the self-signed cert, and reverse-proxies everything — IP-addressed requests and each game domain alike, including the game WebSocket upgrades — to the game server on 127.0.0.1:8788. The installer copies it from the source deploy/Caddyfile, backing up any file already there once.

/etc/caddy/
  Caddyfile                      our config, installed from deploy/Caddyfile
  Caddyfile.pre-gamehoster.<ts>   a one-time backup of any earlier Caddyfile
The Caddy config, and a backup of any earlier one.

/etc/systemd/system/

Two units run at boot. Gamehoster ships its own; Caddy's comes from its package (the installer requires Caddy to be installed from a package so this unit exists) and the installer just enables and restarts it. The game server unit runs as the unprivileged gamehoster user with NoNewPrivileges, ProtectSystem=strict and ProtectHome, and may write only /opt/gamehoster/games and /srv/gamehoster-stats. It has no EnvironmentFile — the process reads its one secret from /opt/gamehoster/token and takes everything else from fixed Environment= lines (port, bind address, the games root, and the log/stats dirs). It is added to the caddy supplementary group so it can read the access logs.

/etc/systemd/system/
  gamehoster.service             the game server — runs /opt/gamehoster/server/server.js
  caddy.service                  the public listener, from the Caddy package
The two service units.