Skip to Content
ScriptingState Bags
Report an error
Open in ChatGPTOpen in Claude

Replicate state without treating it as authorization

A state bag associates key/value data with an entity, player or the server-wide global state. It requires the relevant state-awareness support. The official state-bag contract  describes ownership, shallow serialization and replication; it does not make every value server-authoritative or persist it through a restart.

Select the correct bag

BagUse and default write policy
Entity(entityHandle).stateEntity-related data. Normally writable by the owning client and the server.
Player(serverId).statePlayer-session data. Normally writable by that player and the server. A player’s bag is not their ped entity’s bag.
LocalPlayer.stateThe current client’s player bag.
GlobalStateServer-set state read by clients; not private storage.

Never put API keys, private identifiers or unpublished account data in replicated state. An entity or player going away, or a resource/server restarting, requires an explicit lifetime and restoration design. A database remains the appropriate durable record for a purchase, inventory or account role.

Request replication explicitly

The general manual describes Legacy defaults: a server assignment replicates, while a client assignment remains local. The Enhanced migration contract instead requires explicit replication. Use the runtime’s documented set(key, value, replicated) form deliberately where that transport is required, and test the selected track rather than assuming the defaults match.

-- server.lua; target is a validated, currently connected server session ID. local function publishStatus(target, ready) if not GetPlayerName(target) then return false end Player(target).state:set('docs:ready', ready == true, true) return true end

A local-only assignment using false is not a substitute for keeping sensitive server state in a private server table. Nor does a client’s replicated acknowledgement prove that the client performed the requested action. Read and validate the actual server-owned result before completing a privileged operation.

For global state, the source illustrates GlobalState.someKey = value on the server and reading it on the client. Check the target track’s documented global-state replication behavior before carrying that Legacy example into Enhanced; do not infer an undocumented global setter overload from the entity proxy.

Shallow keys avoid misleading writes

Each getter can deserialize the entire value returned from the bag. Only a direct setter serializes a change back to the runtime. Mutating a nested value such as Entity(vehicle).state.settings.color = 2 is not a reliable replicated write.

Prefer a granular key such as state:set('settings:color', 2, true) or read one structured value, make a validated copy and assign the complete value once. Do not read the same large nested value repeatedly in a frame loop. A state bag is not automatically bandwidth-efficient merely because it replaces an event; payload size and change frequency still matter.

Resolve change-handler context correctly

AddStateBagChangeHandler(keyFilter, bagFilter, handler) returns a handler cookie. The callback receives (bagName, key, value, reserved, replicated). Use value for the incoming value; the previous value can still be visible in the bag while the handler runs. The handler is not a cancellation or authorization hook.

Entity bags and player bags require different resolution helpers. This client example observes a player flag by resolving its player-bag server ID, rather than passing a player bag to GetEntityFromStateBagName. The GetPlayerFromStateBagName native  documents zero as its failure result, which is ambiguous with a valid client index zero; the explicit conversion below preserves that valid index:

-- client.lua; observational output only, not an authority for gameplay privileges. local cookie = AddStateBagChangeHandler('docs:ready', nil, function(bagName, _, value, _reserved, _replicated) local serverId = tonumber(bagName:match('^player:(%d+)$')) if not serverId then return end local player = GetPlayerFromServerId(serverId) if player == -1 or not NetworkIsPlayerActive(player) then return end if type(value) ~= 'boolean' then return end print(('Visible player %d ready: %s'):format(GetPlayerServerId(player), tostring(value))) end) AddEventHandler('onResourceStop', function(resource) if resource ~= GetCurrentResourceName() then return end if cookie ~= nil then RemoveStateBagChangeHandler(cookie) cookie = nil end end)

For an entity bag, use GetEntityFromStateBagName, reject handle zero and check existence before entity operations. Under Legacy, a callback can precede local entity availability; under the reviewed Enhanced contract it fires when the entity exists. Other prerequisites, including collision, ownership or a resource still being active, can still change. Bound retries, retain a generation and re-check identity before applying delayed work.

The change-handler native  supplies the callback contract. A server website test cannot reproduce streaming or an ownership handoff. Resource restart and explicit removal must not register duplicate observers.

Strict mode and server authority

The current manual documents sv_stateBagStrictMode: when enabled, only the server can modify state bags. This changes the default player/owner write policy and can break resources that rely on client acknowledgements or local writes. Test those dependencies on a separate server before changing it.

Strict mode does not validate the business logic that produced a value. A server handler that copies an untrusted client price into a state bag is still unsafe. Keep permission checks, account balances and transaction identity on the server, and use replicated values only for the state clients actually need. See server configuration and secure events.

Verify the lifecycle

Test a server write, an owner write, an unowned-entity write, local-only and replicated values, and the same operations with strict mode enabled. Repeat after changing the ped model, leaving and entering scope, migrating entity ownership, reconnecting with a reused server ID and restarting the resource. The handler must not apply stale data to a different entity or session.

Follow network IDs for conversion boundaries and platform migration for idempotent entity initialization. State bags are a transport for state, not a general RPC acknowledgement, permission system or persistence layer.