Appearance
Server-Only Player Data ​
First-class pattern, not an edge case
The Server table (or any key you list under ServerOnlyRoots) is the normal place for saved, server-only profile fields: they load and save like everything else in profile.Data, but they are removed before the player Replica is built—so no client sees them, not even the owner. For match-wide UI state visible to everyone, use Shared Session instead.
PlayerState supports per-player, server-only data under configured roots (typically a top-level Server table). This data saves and loads with the profile and is available from server APIs, but it is removed before the player Replica is created, so it never replicates to any client—including the owning player.
When to use it ​
Use a Server root (or any key listed in ServerOnlyRoots) for hidden or internal fields that must persist but must not be visible to clients:
- Internal version counters, normalized snapshots, caches
- Anti-exploit or analytics fields you do not want exposed
- Anything you would not put in normal replicated paths for security or design reasons
Naming
The key name Server means “server-only per-player slice of the profile,” not “whole-server state.” For current-server state shared across all clients, use Shared Session.
Configuration ​
In PlayerStateConfig, under Server, list top-level roots that are saved but not replicated:
lua
-- PlayerStateConfig.lua (excerpt)
Server = {
-- ...other settings
ServerOnlyRoots = {
"Server",
},
RuntimeNonPersistentRoots = {
"session",
"_LeaderboardRanks",
"_Leaderboards",
"_Leaderboard",
},
},RuntimeNonPersistentRoots — Not saved; still replicates (for session, etc.).ServerOnlyRoots — Saved; not replicated.
DefaultData example ​
You do not need to introduce Public / Private layout. Keep your existing top-level fields for normal owner-replicated data; add Server only where you need hidden persisted fields:
lua
-- DefaultData.lua
return {
Coins = 0,
Inventory = {},
session = {
isSprinting = false,
},
Server = {
Version = 1,
Cache = {},
LastSeen = nil,
LifeStats = {},
},
}Server behavior ​
lua
PlayerState.SetPath(player, "Server.Version", 2)
print(PlayerState.GetPath(player, "Server.Version")) -- 2
local data = PlayerState.GetAll(player)
print(data.Server.Version) -- 2SetOfflineData can update Server.* because server-only data is persistent (unlike session.*):
lua
PlayerState.SetOfflineData(userId, "Server.LastSeen", os.time())Client behavior ​
Clients always see nil for server-only paths; the data is not in their Replica.
lua
print(PlayerStateClient.Get("Server")) -- nil
print(PlayerStateClient.GetPath("Server.Version")) -- nil
local data = PlayerStateClient.GetAll()
print(data.Server) -- nilOnChanged on the client does not fire for Server.* mutations, since those paths are never replicated.
Warnings ​
Serveris per-player profile data, not server-wide. Use Shared Session for replicated server-wide temporary state.- Do not store secrets in normal top-level replicated fields if the owning client must not read them—use
Server/ServerOnlyRootsinstead.
See also ​
- Data visibility model
- Setup — PlayerStateConfig
- Session Data — non-persistent per-player roots