Client-Server Synchronisation

Gamehoster mirrors your local games to the server over a private, token-protected API on the server's IP. Caddy terminates TLS and reverse-proxies to a localhost-only game server, and the sync client generates each game to static code and then pushes only the files that changed. The read-only endpoints behind the logs and stats browsers share this shape and live on the Tools page.

GET    /api/version
GET    /api/tree
PUT    /api/file
DELETE /api/file
The sync API: discover the version, hash the whole tree, and upload or delete a file.

Private API

Every call but the version probe is authenticated with a bearer token — the gamehoster-config-apiToken from your client configuration, sent as Authorization: Bearer <token>. There is nothing else to the surface: no dashboard, login, session or cookie. A correctly authenticated call to a real endpoint always returns 200 with a JSON object — the outcome, including a failed write, lives in the body. Anything else — a missing or wrong token, an unknown route, or a path pointing outside the games root — returns an identical plain 404, so a probe cannot tell the API is there at all (see Networking). The sync routes sit directly under /api/; the version probe is deliberately unauthenticated so a client can always ask what the server speaks.

EndpointPurpose
GET /api/versionReport the server's API version, so the client can detect skew and pick its path prefix. Public.
GET /api/treeFetch a content hash of every file under the games root, so the client can diff and push only what differs.
PUT /api/fileUpload one file to its path under the games root, creating parents and scheduling a reload.
DELETE /api/fileDelete a file, or a whole game, that is on the server but gone locally.

GET /api/version

Reports the server's wire-contract version. It is public and unauthenticated, so a client whose version has drifted can still ask what the server speaks and turn an otherwise opaque 404 into a clear “upgrade the client” message. It also names the /api/vN prefix the read-only tools routes live under.

Request

MethodGET
Path/api/version
AuthNone — public.
ParametersNone.

Response

A 200 JSON object:

{ "api": 1 }
FieldTypeMeaning
apiintegerThe server's API/protocol version. The client compares it to its own; on a mismatch it reports which side to upgrade.

GET /api/tree

Returns a flat map of every file under the games root to its content hash, so the client can compare hashes file by file and send only what differs. Each hash is sha256 of the file's bytes, computed the same way on both sides, so equal hashes mean identical content. The whole tree comes back in one response — there is no descent — which is cheap because a games root holds a bounded set of small definition and generated files.

Request

GET /api/tree HTTP/1.1
Authorization: Bearer <token>
Fetch the content hash of the whole games root.

Authorization: Bearer <token>; no parameters, no body.

Response

A 200 JSON object mapping each relative path to its sha256:

{
  "game.example.com/asteroids/gamehoster-game.json": "9f2b…",
  "game.example.com/asteroids/gamehoster.server.js": "1a77…",
  "game.example.com/asteroids/gamehoster.client.js": "3c04…"
}
One sha256 of the file's bytes per file under the games root. The client hashes its own files the same way and takes the difference.
FieldTypeMeaning
<path>stringEach key is a file's path relative to the games root; each value is the sha256 hex digest of its contents. An empty root returns {}.

PUT /api/file

Uploads one file — creating or overwriting it — at a path under the games root, creating any parent directories as needed. The write is confined to the games root: a path escaping it gets the uniform 404. Every write schedules a debounced reload, so a burst of files pushed together triggers a single re-read of the changed games rather than one per file.

Request

PUT /api/file?path=game.example.com/asteroids/gamehoster.server.js HTTP/1.1
Authorization: Bearer <token>
Content-Type: application/octet-stream

<file bytes>
Upload one file to its path under the games root.
ParameterInMeaning
pathqueryThe file's path relative to the games root. A path that normalises outside the root is rejected with the uniform 404.
(body)bodyThe raw file bytes.

Method PUT, plus Authorization: Bearer <token>.

Response

A 200 JSON object describing the write:

{ "ok": true, "size": 20144 }
FieldTypeMeaning
okbooleantrue on success; false with an error string when the write failed.
sizeintegerThe number of bytes written on disk.
errorstringPresent only when ok is false.

DELETE /api/file

Removes a file, or a whole directory subtree (a game), that exists on the server but is gone locally. Like the write, it is confined to the games root and schedules a reload. Deleting a path that is already absent is still a success — status codes carry no meaning here.

Request

DELETE /api/file?path=game.example.com/asteroids/old-entity.js HTTP/1.1
Authorization: Bearer <token>
Remove a file that is gone locally.
ParameterInMeaning
pathqueryThe relative file or directory path to remove. A path escaping the games root gets the uniform 404.

Method DELETE, plus Authorization: Bearer <token>; no body.

Response

A 200 JSON object:

{ "ok": true }
FieldTypeMeaning
okbooleantrue once the path is gone (or was already absent).

Synchronisation Process

Synchronisation is content-addressed: the client hashes its files, asks the server for its hashes, and sends only the difference. It begins with a generate step that turns each game's definition into the two static files the server runs — this is where the “generated, not interpreted” contract is met, so the server only ever requires ready code. A run is stateless: it is driven by client/sync.js, recomputing everything each time.

1

Generate the static code

For each game, regenerate its gamehoster.server.js module and gamehoster.client.js library from the definition, written into the game folder — so the ordinary push carries them and the server never compiles at request time.

2

Negotiate the version

GET /api/version — read the server's version. A skew turns into a clear “upgrade the client”, not a silent misparse.

3

Diff by content hash

GET /api/tree — fetch the server's hash of every file and compare it to the local hashes. Files whose sha256 differs are the upload list; managed server files absent locally are the delete list.

4

Push new & changed files

PUT /api/file?path=<rel> — send the raw bytes of each new or changed file. Because the hash is of the contents, the next run's hashes match with no timestamp to keep in step.

5

Prune what's gone

DELETE /api/file?path=<rel> — anything the server holds under a <domain>/<game>/ it manages, but which is absent locally, is removed.

Server reloads, debounced

Each write and delete schedules a short debounce; when it fires the server re-reads the games that changed and swaps to the new code. Nothing stays resident; the client watches no files unless you pass --watch.

What syncs: structure, not a blocklist

What travels is decided by the shape of the tree, so there is nothing to configure and no way to leak a stray file. The client mirrors exactly <root>/<domain>/<game>/**:

  • Domain. A directory whose name contains a dot and does not start with _ or . — e.g. game.example.com. Loose files and non-dotted directories at the root are left alone.
  • Game. A directory inside a domain whose name does not start with _ or . and holds a gamehoster-game.json. Its whole subtree is taken, skipping only hidden entries and node_modules.
  • Reserved. Any _-prefixed name (a future _shared/ library, say) is neither pushed nor pruned, so the server can hold things the client does not own.
  • Prune-safe. Only a path at least <domain>/<game>/ deep may be deleted on the server. Everything shallower — reserved dirs, loose top-level files — the server keeps untouched, so a sync can never remove what it did not put there.

Why it stays correct and cheap

  • Content-addressed. Every hash is sha256 of the file's bytes, computed the same way on both sides, so identical content always agrees and any real edit is always caught — with no mtime to keep in step. The client is stateless; the server hashes from disk on each request.
  • Generated ahead of time. Generation happens on your machine before the push, so the server loads static modules with no new Function at request time.
  • One small surface. A bearer token guards every call but the version probe; there is no dashboard, login, session or cookie to attack.
  • Status codes say nothing. Every authenticated call returns 200 with an object, even a failed write. Anything else returns an identical 404, so a probe can't tell an API is there at all.