Permissions – locking down net.state¶
By default net.state trusts every client: any peer can write any key, and games stay
honest by ownership convention. That is fine for a friendly game, but a
determined client could just do net.state.winner = net.id() and win. This tutorial turns
that convention into a rule the host enforces, using the MULTIPLAYER tab.
We build on the Build a Coin Rush game – have it working first.
The two flags¶
Every net.state path has two client permissions, set in the MULTIPLAYER tab and
enforced by the host at runtime:
Clients can write – when off, only the host may write the path. A client’s write is applied optimistically and then rolled back when the host’s rejection arrives, so a
net.onchange listener sees the value flip and flip back.Clients can read – when off, the host keeps the path private: it is never sent to clients, in the join snapshot or in live updates.
Three things to keep in mind:
Allow-by-default. A path you never configure is fully open. You opt into restrictions; you never have to grant permissions just to keep a game working.
Inheritance. A path inherits the flags of its nearest configured ancestor, so locking
playerslocksplayers.3.xtoo – configure at the granularity you actually own.The host is the authority. There is no separate server to restrict. “Client” always means a joined peer.
Keeping state server-private¶
Turning Clients can read off keeps a path on the host only. Use it for anything a client should not be able to inspect – a shuffled deck, an AI’s target, an unrevealed answer:
-- host only
net.state.deck = shuffle(make_deck()) -- with "Clients can read" off on "deck",
-- clients never receive it
net.state.top_card = net.state.deck[1] -- reveal one card through a readable key
Clients simply never have net.state.deck in their store; reading it returns nil. The
host reveals what it wants through separate, readable keys.
What to lock down¶
A good rule of thumb: the host owns anything global or authoritative – the winner, whose turn it is, spawned pickups – and clients own only their own id-keyed branch (their position, their inputs, their own score). Mark the global keys host-only and you have turned the ownership convention into something the engine guarantees.
Note the distinction: in click-race each player’s score lives inside its own id-keyed branch
(net.state.players[net.id()].score) and is written by that client, so it stays open – it
is the winner derived from those scores that is global and belongs to the host. Lock the
authoritative fact, not the per-player data that feeds it.