Access Control
Last updated: 26 August 2026
Access control decides who is allowed to touch each table. It answers a different question from authentication: sign-in establishes who the caller is; access control establishes what that caller may do. The model is deliberately simple — Read and Write per table, Execute per function, for three kinds of caller — composed with per-player row ownership.
By default a game client can read but not write, and a brand-new table is closed to the client entirely. You open access up deliberately, per caller and per table. If a client call is refused for lack of access, the runtime is the authority — a client-side check may refuse early, but it never grants.
The three subjects
Every runtime call resolves to one subject, taken from the credential it already carries.
| Subject | Who it is |
|---|---|
| client | Your game client — the public pk_... key, with or without a signed-in player |
| server | Your own server — a secret sk_... key (game servers, scripts, CI) |
| backend | Your server-side Functions — the cloud-function plane |
The Backoffice (operator) plane is separate and is never limited by these table settings.
Two layers that compose
Access is decided in a fixed order: authority first, then row scope.
- Table authority — may this subject read or write this table at all? If not, the call is refused with
403and nothing else is considered. - Row scope — for a client on a table owned by a player, which rows are visible? A player sees its own rows; another player's row is invisible.
Table authority
Each table carries a Read and a Write flag for every subject. Write covers every mutation — create, update, and delete alike; there is no separate delete permission.
| Subject | Default Read | Default Write |
|---|---|---|
| client | off | off |
| server | on | on |
| backend | on | on |
A newly created table starts closed to the client on both axes and open to server and backend. This is why a client Update() on a fresh table is refused until you grant client.write — the write boundary is the safe default, not a bug.
Granting client.write on a table that is not owned by a player lets any client write any row of it — the shared/world-state case. Only tables owned by a player scope client writes to the caller (see below). Grant open client write deliberately.
Row ownership for clients
On a table marked owned by player, table authority still decides whether a client may touch it; row ownership then decides which rows.
- Read
owner— a client sees only its own rows. - Read
public— a client sees every row (ownership does not scope reads). - Write — a client create stamps the acting player as owner; an update or delete only succeeds on the caller's own row.
- server and backend are never scoped — they read and write any row.
A row owned by another player is treated as invisible, not as "forbidden": reading, updating, or deleting it returns 404 — the same result a row that does not exist returns. This is intentional, so the write path never reveals a row the read path hides.
Because a foreign row and a missing row are indistinguishable by design, don't rely on the difference between them in client code — both come back as a plain not-found.
Function execute
Each Function carries an Execute flag per subject — client, server, backend — all on by default. A subject without Execute is refused with 403.
Execute is separate from the function's Auth requirement (none or player): Auth is whether an anonymous client may call, Execute is whether the subject is allowed at all. A client call to a player-auth function with no signed-in player is refused with 401 regardless of Execute.
When a call is refused
| Situation | Result |
|---|---|
| Subject lacks Read on the table | 403 table_read_forbidden |
| Subject lacks Write on the table | 403 table_write_forbidden |
| Subject lacks Execute on the function | 403 function_execute_forbidden |
| Anonymous client reads an owner-scoped table | 403 owner_read_requires_player |
| Anonymous client writes an owned table | 422 acting_player_required |
Client calls a player-auth function with no player | 401 player_auth_required |
| Client touches a row owned by another player | 404 not_found (invisible) |
| Client reads another player's account | 403 player_read_forbidden |
If a client write is unexpectedly refused, check the table's client.write flag first, then whether the table is owned by a player and you are acting as the row's owner. The two together explain almost every 403/404 on the data plane.
Next steps
- Data Mutation & Model Extending — the
Update()/Delete()calls these rules gate - Schema Object Reference — the owned-by-player setting and read policy on a table
- Functions — where per-function Execute and the Auth requirement are configured