Host authority
Exactly one peer at a table is the host. The host owns the table's real state: it holds the
physics world, it is the only peer that applies a change, and it broadcasts the result to
everyone else. Every other peer is a player or a spectator and renders what the host sends.
A table script runs on the host and nowhere else. The start path refuses to run when the
peer's role is player or spectator (apps/web/src/ui/App.tsx, startTableScripts —
"Scripts execute only where the table is authoritative: the room host, or a solo (offline)
table. Players/spectators receive effects via snapshots."). A solo table has no other peers and
is authoritative over itself, so scripts run there too.
What this means if you are not the host#
You are reading a script's behavior from the outside. Your client never loads the script, never evaluates a handler, and never sees a diagnostic it produced. What reaches you is the outcome: the host applied a change, and the change arrived in a state broadcast. Three consequences are worth holding on to:
- Timing is the host's, not yours. A script that waits two seconds waits two seconds on the host's clock. You see the result when the following broadcast lands.
- A script's diagnostics are invisible to you. Script errors and
world.loglines are written to the host's own event log. If a game is misbehaving and you are not hosting, the person who can see why is whoever is. - Nothing a script does is trusted from your side. You cannot influence a script by sending anything, because your client is not running it.
The path a change takes#
Every table-script mutation travels the same route. Nothing skips a step.
your handler (runs in the script sandbox, on the host)
handle.flip()
-> postMessage to the host ("intent", plus which script sent it)
-> validateScriptIntent (host-side: spawn / object-action / transform only)
-> runtime.applyIntent (physics + state change, attributed to "Script")
-> emitSnapshot (host broadcasts to every peer)
-> each peer renders the new state
Two of those steps are gates you can actually hit:
- The intent allowlist. A table script can emit exactly three intent types —
spawn,object-actionandtransform(apps/web/src/scripting/TableScriptHost.ts,validateScriptIntent). Anything else is refused with a diagnostic in the script console, and the object-action name is re-checked against the script allowlist even though the in-frame API emits only allowed names. See Action vocabularies. - Schema validation. A
spawndefinition is parsed with the shared table-object schema before it reaches the runtime. A definition that fails — an over-long tag is the usual cause — is dropped whole, with a diagnostic and no object. See Known limitations.
Who the actor is#
Every lifecycle event carries an EventContext whose actor says who caused it. The values the
runtime actually passes are:
actor |
What caused the event |
|---|---|
| a peer id | A participant acted. This is the common case at a real table, and it covers the viewer's own grab tool and VR hands as well as a remote player's intent — the grab paths report the viewer's peer id (apps/web/src/playcanvas/TabletopRuntime.ts, currentActorLabel). |
"Script" |
A table script emitted the intent (apps/web/src/ui/App.tsx, the TableScriptHost dispatchIntent callback). |
"Host" |
The runtime's own default — an internal path that named no actor (applyIntent(intent, actor = "Host")). |
"You" |
The local viewer where no peer id exists yet — a solo or offline table. currentActorLabel() returns viewerPeerId ?? "You". |
"System" |
The runtime's other offline label, treated as local alongside "You" (isLocalActor). |
The spelling is capitalised, and the declaration says so
(packages/shared/src/scripting.ts, EventContext.actor). A handler comparing
context.actor === "script" never matches; compare against the four literals above and treat
anything else as a peer id.
Mod scripting is gated per method, not per peer#
Mod scripting does not have the single host-only switch table scripting has. A mod's api
methods fall into three groups instead, and the badge on each entry tells you which:
- Methods that dispatch an intent —
createObject,objectAction,setObjectSound— go through the host exactly like a table script's mutations. - Methods that are refused off the host —
setUiElementanddeleteUiElementreject with"Only the host can update mod UI state."on aplayerorspectatorpeer. - Methods that are answered inside the sandbox frame —
getMySeat,getMyTeam,getTurnandonpost nothing at all and read context the host pushed in, so their answer is about the peer that asked.
The three authority values#
The authority badge on every reference entry answers one question: whose copy of the table changes, and who has to be running?
host-authoritative#
The call emits a TableIntent. The host validates it, applies it, and the effect reaches every
peer in the next state broadcast. Two entries carry this badge without emitting an intent,
because their effect still reaches everyone anyway: world.broadcast, where the host posts the
chat line to every peer, and api.playSound, an ephemeral sound event sent on the unreliable
channel. Neither one is table state, and neither survives a save.
host-only#
Nothing replicates. Either the host-side implementation refuses the call for a non-host role,
or the call is a read back from the host, or it writes to the host's own console. world.log
and api.registerAction are the plainest examples: both land in the host's local state and no
other peer learns anything.
all-peers#
The call is answered entirely inside the sandbox frame, on whichever peer is running the script. No message is posted and nothing replicates, so the answer describes that peer.
By design.
all-peersis never correct on a table-scripting entry, because a table script only ever runs on one peer — the host — so "all peers" and "the host" would name the same thing and the badge would carry no information. Every table-scripting member ishost-authoritativeorhost-only. If you are reading a table-scripting entry badgedall-peers, the page is wrong; please report it.
See also#
- Async and snapshots — what "resolves" means when the answer arrives by broadcast.
- Execution order — when your code runs relative to the table it is reading.
- Events and delegates — which peer fires each event and how many times.
- Action vocabularies — the three action lists the host enforces.
world.spawnObject— a worked example of an optimistic, host-authoritative call.ObjectHandle— the twelve mutators that return before the host has applied anything.- Events — which peer fires each
globalEventsdelegate. - Mod hooks and capabilities — the same authority question on Surface B.
- Intents — the wire shape a mutator turns into.
- Mod scripting API — the 21 mod methods, each badged with whose copy of the table its effect reaches.
- Types —
PlayerInfo.isHost, how a script asks which peer it is running on.
