Skip to main content

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.

info

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.

SubjectWho it is
clientYour game client — the public pk_... key, with or without a signed-in player
serverYour own server — a secret sk_... key (game servers, scripts, CI)
backendYour 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.

  1. Table authority — may this subject read or write this table at all? If not, the call is refused with 403 and nothing else is considered.
  2. 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.
Call + credential→ subjectauthorityRead / Write flagno → 403 forbiddenrow scopeClient on owned tableown rows onlyForeign row404 — 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.

SubjectDefault ReadDefault Write
clientoffoff
serveronon
backendonon

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.

warning

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.

note

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​

SituationResult
Subject lacks Read on the table403 table_read_forbidden
Subject lacks Write on the table403 table_write_forbidden
Subject lacks Execute on the function403 function_execute_forbidden
Anonymous client reads an owner-scoped table403 owner_read_requires_player
Anonymous client writes an owned table422 acting_player_required
Client calls a player-auth function with no player401 player_auth_required
Client touches a row owned by another player404 not_found (invisible)
Client reads another player's account403 player_read_forbidden
tip

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​