Update

Every instance advances one tick at a time, and a tick is a pure function: it takes the previous state of everything and produces the next state, with no hidden inputs. It runs in exactly three steps: handle the commands that arrived, let every player and entity update itself, then run the one global instance update. Because it is pure and its order is fixed, the server and the front end compute the very same tick.

one tick  =  previous state  →  next state   (a pure function)

  1  HANDLE COMMANDS    each command's handler runs — updates the sender's player
                        state and/or instance state (legality = the handler ignoring)

  2  UPDATES            every player and entity runs its update, in parallel:
                        reads the PREVIOUS state, writes ONLY its own state

  3  INSTANCE UPDATE    the one global pass — spawn/destroy, cross-cutting logic,
                        react to flags set in step 1, run the phase machine
The three steps of a tick: commands, then per-actor updates, then the one global instance update

Each actor's update is split not by side but by trust. The shared, predictable part lives in -update-frontend.js and runs on both sides: on the front end (as prediction) and on the backend (as the first part of its work). The private, authoritative part lives in -update-backend.js and runs only on the backend, after the shared part. So the backend runs the superset (the shared file, then its own) while the front end runs just the shared file. The shared logic exists once and cannot drift. The three steps below are that whole backend tick; the clocks, the three kinds of state, per-player relevance and random then describe how it reaches each player, and the checks that keep the split honest.

Step 1: handle commands

Every command a player sent this tick runs its shared gamehoster-command-handler-frontend.js, then, on the server only, any gamehoster-command-handler-backend.js. A handler reads its inputs with Gamehoster.Command.Get(param), takes the sender as Gamehoster.Context.playerId, and writes the sender's player state and/or instance state. This is the only step driven by player input.

There is no separate legality check. Legality is the handler ignoring what the player's state disallows: pong's moveY returns early unless the instance is playing, so an out-of-turn or tampered command simply does nothing:

// gamehoster-commands/moveY/gamehoster-command-handler-frontend.js
const me   = Gamehoster.Context.playerId
const here = Gamehoster.Context.instanceId
if (Gamehoster.Instance.State.Get(here, "phase") !== "playing") return   // legality = ignore

const y  = Gamehoster.Player.State.Get(me, "y") + Gamehoster.Command.Get("axis")
            * Gamehoster.Instance.State.Get(here, "paddleSpeed")
Gamehoster.Player.State.Set(me, "y", y)   // writes the sender's own state
A command handler updates the sender's state; an illegal command is one the handler ignores

Alongside its handler, each command declares the fields it writes: pong's moveY ships a gamehoster-command.json of { "writes": ["player.y"] }. That declaration feeds the deploy check: together with each field's sync it lets the engine prove, before the game runs, that every shared field is one a client can actually reproduce, and reject as shared anything it can't.

Step 2: updates, in parallel

Every player and every entity runs its own update once. First the shared -update-frontend.js (this runs on both sides) then, on the backend only, any -update-backend.js for that actor. They all read the same previous state (the snapshot the tick began with) and each writes only its own state. Nothing sees another actor's half-finished changes, so they are effectively updated at the same time.

Because no update depends on the order the actors happen to be visited in, and none can touch another's state, the result of the step is identical however it is scheduled. That order-independence, plus the shared file being one function run on both sides, is what lets the server and the front end compute the same tick: the foundation prediction and reconciliation are built on.

// gamehoster-entities/ball/gamehoster-entity-update-frontend.js  (shared: both sides)
const me = Gamehoster.Context.entityId
const x  = Gamehoster.Entity.State.Get(me, "x")   // read the PREVIOUS state
const y  = Gamehoster.Entity.State.Get(me, "y")
Gamehoster.Entity.State.Set(me, "x", x + Gamehoster.Entity.State.Get(me, "vx"))   // write OWN state only
Gamehoster.Entity.State.Set(me, "y", y + Gamehoster.Entity.State.Get(me, "vy"))
The ball's whole update is shared, so it needs only the front-end file: no -update-backend.js

Step 3: the instance update

After every actor has stepped itself, the instance takes its one global pass. It, too, is split by trust. The shared gamehoster-instance-update-frontend.js runs on both sides for the cross-cutting logic a client can reproduce: pong's wall-and-paddle bounce. Then, on the backend only, gamehoster-instance-update-backend.js does what a client cannot: spawn and destroy entities, keep score, react to flags step 1 set (a ready that should start the match), roll an authoritative serve with Entropy(), and run the phase machine from lobby to playing to over.

// gamehoster-instance/gamehoster-instance-update-backend.js — the backend-only extra,
// run AFTER the shared bounce. It owns what the client can't predict.
const here    = Gamehoster.Context.instanceId
const players = Gamehoster.Player.List()

// react to a flag set by the ready command in step 1
if (Gamehoster.Instance.State.Get(here, "phase") === "waiting"
    && players.length === 2
    && players.every(id => Gamehoster.Player.State.Get(id, "ready"))) {
  Gamehoster.Instance.State.Set(here, "phase", "playing")
  Gamehoster.Entity.Spawn("ball", {})       // spawn — a global action, backend only
}

// cross-cutting outcome: a goal no single actor owns (the ball was already bounced
// by the shared pass this tick)
const ball = Gamehoster.Entity.List("ball")[0]
if (ball !== undefined && Gamehoster.Entity.State.Get(ball, "x") < 0) {
  const r = players.find(id => Gamehoster.Player.State.Get(id, "side") === "right")
  Gamehoster.Player.State.Set(r, "score", Gamehoster.Player.State.Get(r, "score") + 1)
}
The backend-only instance pass: spawn/destroy, scoring, phase and serve; the predictable bounce lives in the shared front-end file

When it returns, the tick is done. The state it leaves behind becomes the previous state the next tick reads. Because the whole cycle is a pure function of that previous state, replaying the same commands always reproduces the same tick: on the server, and on every front end for the shared part it runs.

Three clocks: the tick rate, the send rate and rendering

The three steps above run at the tick rate (gamehoster-game-tickRate, say 60 a second) and they run at that same rate on the server and on every client, because the tick is pure. The server does not send a packet every tick. It sends at the lower send rate, gamehoster-game-sendRate (say 20 a second), and between packets each client keeps ticking on its own, predicting forward from what it last heard. Rendering is a third, independent clock: the front end draws as fast as it likes from an interpolated view, with no fixed rate at all.

tickRate  (e.g. 60/s)   the pure simulation step — server AND every client
sendRate  (e.g. 20/s)   how often the server emits a packet, per player
render    (free-running) the client's own draw loop, from an interpolated view
Three clocks: the sim ticks fast on both sides, the wire is slow, the screen is free

Sending less often than we simulate is the whole game: the wire carries as little as possible, and the client fills the gaps by running the same tick locally. What must cross the wire, and how, is decided field by field, by the kind of state each field is.

Three kinds of state

Every field in a schema declares how the client gets it (its sync) and there are exactly three answers. This is the whole of the netcode; there is nothing else to tune.

syncComputedOn the wireExample
serverbackend onlynever senta hidden dice roll, matchmaking scratch, an AI's target
sentbackend, from data only it haspushed to clients that can see it; the client takes it as truth (snaps / reconciles)the score, a spawn, a fog reveal
sharedboth sides, by running the same commandsnot sent as a value: reproduced from commands, corrected by an occasional keyframea paddle's y, the ball's position

A field also declares its audience: owner (only the player it belongs to) or all (everyone it is relevant to), and an optional smooth for render interpolation. The full table of sync/audience/smooth, with the exact combinations, is on the Players page. The three kinds are also what a front-end body may touch: it can read sent and shared fields of the things relevant to it, plus its own owner fields, but never a server field, because that value simply isn't on the client to read.

The front end runs the shared half

Between the sparse send-rate packets, the client keeps ticking on the -update-frontend.js files (the same files the backend runs, not a copy) so the shared (and own owner) fields it can compute from the commands it holds stay live at the full tick rate. It never runs a -update-backend.js: it has no server state to feed one. When the next packet arrives the engine reconciles: sent fields snap to the server's word, shared fields are corrected toward the keyframe and (where smooth is set) eased rather than jumped. Because the shared file is one function that ran on both sides, those corrections come only from the backend's private pass, and are normally tiny.

What the player sees is drawn a little behind this, so it eases toward known state instead of guessing past it. Your own player and deterministic entities are drawn at the present, other players and sent state a little behind, set by gamehoster-game-displayDelay. The full display model is on the Frontend internals page.

Each player sees only their slice

A packet is per player, not a broadcast, and a player may not be able to see the whole instance. Which players, entities and streamed geometry a given viewer can see is relevance, decided by an optional gamehoster-player-view.js: it runs on the backend for each viewer and marks what that viewer may receive (including what they will need soon, so it arrives before it is wanted). With no view script, everything is relevant to everyone, which is why pong and the other small examples never write one.

So each player's packet is a small, tailored stream: the relayed commands of the players relevant to them (which reproduce those players' shared fields locally), plus pushes of the sent fields, enter/leave as things cross into and out of relevance, and the odd keyframe.

Partial visibility raises one sharp question: what if something a player can't see affects something they can: a door they see, opened by a plate hidden in the fog? The answer is a rule about what may be predicted, not a runtime patch. A field may be shared only when every client that sees it is guaranteed to hold all its inputs: when it is owner-driven and reads only its own object, always-present instance globals, and its owner's commands. Anything whose inputs could be hidden must be sent. So the door is a sent field: it opens on the server's authoritative word, identically for everyone who can see it, with the cause left hidden. Prediction is reserved for what a client can always reproduce; everything else is authority.

This is why a shared field cannot drift from a missing input: by construction it never has one. A client is only ever "wrong" about objects it was deliberately not sent, which is the whole point of the fog. There is no per-client shadow simulation and no override machinery: the boundary is a checkable property, not a mechanism. And results of server-only logic still reach clients just one way: the backend writes them into a sent field or a spawn; the private working state stays behind.

Predictable and unpredictable random

Because shared logic must land on the same answer on both sides, randomness comes in two flavours:

FunctionWhat it isWhere
Gamehoster.Random()Predictable: seeded by the tick and the current actor, so it returns the same value on server and client. Safe inside shared logic; it is a pure function of the tick.backend and front end
Gamehoster.Entropy()Unpredictable: true randomness. Drives authoritative decisions whose outcome must therefore live in a sent field or a spawn.backend only

So a coin-flip that decides who serves is Entropy() on the backend, and its result (the ball's launch velocity) travels as state; a scatter that must look identical on every client is Random(), computed in step with the server. Cosmetic-only randomness (particles, screen shake) has no place in a predict body at all: it belongs in the render loop, where nothing it touches is synced.

Verified before it runs

The split only holds if a shared body never reaches for something a client doesn't have. Gamehoster checks that for you, and will not synchronise a game to the server if the checks fail: a broken definition is a deploy error, not a live desync. It works in two layers, because the meaning of arbitrary code can't be read from the outside:

LayerWhat it does
Static, from the schema and declarations: a deploy checkEvery field a body names exists and is correctly typed; a -update-frontend.js or a shared command handler (-frontend.js) is not declared to reach server fields; a -backend.js body may; the sync/audience combinations are legal and each command's writes names real fields; the predictability rule holds: a shared field is provably reproducible by every viewer, or it must be sent; and Entropy() never appears in a shared body. Anything it can't prove safe, it forces to sent. A failure blocks the deploy: nothing broken ever reaches a live game.
Runtime, in the Gamehoster.* abstraction: never a crashThe abstraction distinguishes two things. A genuine fixed-schema violation the deploy check couldn't see (a field name built at runtime) is a contained throw: the engine catches it, drops that body's effects for the tick, and surfaces it: it never takes down the server or a client. A view-boundary condition is not an error at all: reading an object out of the client's view simply finds nothing (it isn't in its lists), and a write to a field outside a body's class simply has no effect: the authoritative value stands.

So the declarative layer forces anything it can't prove safe to sent and blocks a broken deploy outright, and the abstraction keeps a running game graceful: throwing only on a real schema violation, and quietly ignoring the ordinary boundaries of what a client can see. Between them, "predict on the front end, keep the private truth on the backend" is a boundary you can't cross by accident and can't crash by trying.