Skip to content

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) -- 2

SetOfflineData 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) -- nil

OnChanged on the client does not fire for Server.* mutations, since those paths are never replicated.

Warnings ​

  • Server is 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 / ServerOnlyRoots instead.

See also ​

PlayerState - High-Performance Roblox Data Management