PlayerState: ProfileStore + Replica, Without the Headache

PlayerState: Player Data That Just Works

A production-ready wrapper for ProfileStore + Replica. Stop reimplementing the same data management boilerplate and start building your game.

📦 Get the Model · 📖 Documentation · 📚 GitHub


What it does

PlayerState combines ProfileStore (data persistence) and Replica (real-time client synchronization) behind a clean, type-safe API.

It handles all the difficult parts of player data automatically:

  • Automatic session management
  • Data validation and reconciliation
  • Real-time client syncing
  • Change detection events
  • Batch operations for performance
  • Leaderstats and global leaderboards
  • Offline player data access

You focus on your game logic, PlayerState handles the infrastructure.

📖 The documentation includes examples for every function, plus advanced patterns for currencies, inventories, progression, leaderboards, and more. Browse the docs →

-- Server
PlayerState.Set(player, "Coins", 1000)
PlayerState.SetPath(player, "Inventory.Weapons", {"Sword", "Bow"})

-- Client
local coins = PlayerState.Get("Coins")

PlayerState.OnChanged("Coins", function(newValue, oldValue, changeInfo)
    updateCoinsUI(newValue)
end)

That's the entire workflow. ProfileStore handles persistence, Replica handles networking, and PlayerState handles the integration.


Getting Started

⚠️ Required: You must call PlayerState.Init(player) on the server when each player joins.

-- ServerScriptService
local PlayerState = require(ReplicatedStorage.Libraries.PlayerState.PlayerStateServer)

game.Players.PlayerAdded:Connect(function(player)

    local success = PlayerState.Init(player)

    if success then
        PlayerState.Set(player, "Coins", 0)
    else
        warn("Failed to load data for", player.Name)
    end

end)
-- Client
local PlayerState = require(ReplicatedStorage.Libraries.PlayerState.PlayerStateClient)

local coins = PlayerState.Get("Coins")

PlayerState.OnChanged("Coins", function(newValue)
    updateCoinsUI(newValue)
end)

Full setup guide: playerstate.netlify.app/installation


Key Features

Global Leaderboards

Track stats automatically and query leaderboard ranks without writing OrderedDataStore code.

local leaderboard = PlayerState.GetLeaderboard("Coins", 10)

for _, entry in leaderboard do
    print(entry.rank, entry.userId, entry.score)
end

Leaderboards update automatically when tracked stats change.

Offline Player Data

Modify data for players even when they are not in the game.

PlayerState.SetOfflineData(userId, "Coins", 5000)

This enables moderation tools, admin panels, refunds, and automated systems.

Batch Operations

Update many values efficiently in a single operation.

PlayerState.BatchSetValues(player, {
    {path = "Coins", value = 500},
    {path = "XP", value = 200},
    {path = "Level", value = 5}
})

This reduces replication overhead and improves server performance.

Runtime (Non-Persistent) Data

Define session-only values that sync to the client but never save to DataStore.

session.isChopping
session.isInCombat
session.currentQuest

Perfect for gameplay state that should reset every session.

Legacy Data Migration

Automatically migrate existing DataStore systems into PlayerState.

Supports multiple migration strategies including overwrite, merge, and continuous migration.


Performance

Tested under heavy load conditions:

Server throughput600,000 ops/sec
Batch processing300,000+ ops/sec
Client data access3M ops/sec
Client response timeSub-microsecond
Concurrent players1,000+

PlayerState's caching and batching layer means it often outperforms raw ProfileStore/Replica implementations.


What you can build

  • Multi-currency systems
  • Inventory systems with nested metadata
  • Progression systems (XP, levels, skill trees)
  • Persistent worlds and base building
  • User settings and preferences
  • Global leaderboards
  • Admin tools with offline data access
101 Likes

Download link doesn’t work : (

4 Likes

Sorry about that, forgot I have to enable it for the creator store. Should be up now for public.

2 Likes

Is there a way to access ProfileStore data directly with this, or am I required to use the wrapper? the .Set() and .SetPath() thing doesn’t seem quite comfortable.

2 Likes

You should be able to use the PlayerState.GetProfile(player) via the server to get the ProfileStore object itself. Keep in mind it would be in .Data so you’d need to access that to get the data.

local playerStore = PlayerState.GetProfile(player)

print(playerStore.Data)
1 Like

This is honestly AMAZING. I’ve always had issues trying to manage player data, and I often find myself never happy with any results I can produce, along with the lack of official documentation for Replica. This is a HUGE lifesaver, and I will most definitely see myself using this on my current projects.

7 Likes

For some reason while executing scripts using the command bar IsReady and IsPlayerDataReady are always false so u cant do anything, please fix. Otherwise great work!

3 Likes

I have tested this myself and I can almost 100 percent confirm this is a Studio limitation - the command bar creates separate module instances that can’t seem to access your game’s data.

For testing PlayerState, I’d recommend using a temporary script in StarterPlayerScripts instead of the command bar. Your actual game scripts all share the same PlayerState instance and work perfectly.
Command bar simply isn’t compatible with stateful modules like PlayerState due to Studio’s execution context isolation.

I have just fixed a bug though where the client Get, GetAll, etc. would run before the data was replicated. This should be fixed now, and the client should wait until the data is replicated. Should no longer return nil if ran right at the top of a local script.

2 Likes

Yes ty I though I needed to make a while true do if PlayerState.IsReady() then break end for every one of my scripts

2 Likes

:smiley: As long as you have re-imported the model from toolbox into your game, should work without having to do that now.

2 Likes

So i stumbled across this and this seems really cool but what if one wants certain data values to not replicate?

1 Like

Currently, the module replicates all player data to the client and there’s no built-in filtering for specific values. However, you have a couple options:

  1. Structure your data - Keep sensitive/server-only data in a separate system outside PlayerState. So like non-saving data just in a table in your module for example.

  2. Client-side filtering - Don’t display certain values in your UI even though they’re replicated

I’m considering adding selective replication in a future update
The underlying Replica system does support selective subscriptions at the replica level, so adding field-level filtering is technically feasible. The current approach prioritizes simplicity and performance - replicating everything avoids the complexity of managing partial state synchronization.

Would selective replication for specific data fields be valuable for your use case?

1 Like

Yes in my case it would be useful to have selective replication as certain datas i dont want shown or given to the client at all and the data i dont want replicated is still data thats meant to be saved

I understand your use case, but after analyzing the performance implications, I’ve decided against adding selective replication. Filtering on every data operation would add significant overhead to the module.
Since PlayerState only replicates each player’s data to themselves (not to other players), you can store everything in PlayerState and simply not display the sensitive values in your client UI. The data gets saved automatically but remains unused client-side.
While exploiters could potentially script to read their own data, they can only GET it - all modifications still go through server validation, so there’s no actual security vulnerability.

Would this approach work for your use case or am I missing something?

My use case has to do with hiding certain datas from exploiters so they cant figure out secret progression stuff

1 Like

Can I get a bit more description on what your trying to do? Assuming it’s something like hidden progression, my system should allow you to set a key that doesn’t exist in default data. So for example like a PlayerState.SetPath(player, "HiddenProgression.SpecialUnlock", 250) or something (At minimum, a blank HiddenProgression table would need to exist in default data for it to not warn and fail set), where HiddenProgression would not be added to their data, until they find that progression.

If you wanna talk about it more and maybe find a work around, or something I can add to help, DM me on discord at bellaouzo or message me on devforum :slight_smile:

well simply speaking i just dont want specific data values to be replicated to the client, while i do appreciate your offer to help i don’t currently have your module implemented but i saw this was a very cool resource and thought about switching to it but sadly it would seem like extra work currently to switch rather than stay with my current setup, however i do look forward to how far

Really like what you made here. I’ve got a question though.
On the clientt you could do

PlayerStateClient.OnChanged("Gems.Maximum", function(newValue, oldValue, info)
	print(newValue, oldValue, info)
end)

In order to get the changes on the client. How would I be able to do something like this on the server.

I currently have this code on the server

RunService.Heartbeat:Connect(function()
	Profile.SetPath(player, "Xp.MaxValue", (100 * Profile.Get(player, "Level")))
	if Profile.GetPath(player, "Xp.Value") >= (100 * Profile.Get(player, "Level")) then
		Values.LevelUp(player, 1)
	end
end)

But I want to do something similar to listening to changes to the value on the server and I cant really find anything like that in the docs.

Yeah I’ve thought about this! I actually had server-side OnChanged implemented at one point, but removed it for performance reasons - having many server listeners can create overhead.

The recommended pattern for server-side logic is to use explicit functions that handle side effects when making changes. For example:

local function GiveXP(player: Player, amount: number)
    PlayerState.SetPath(player, "XP.Current", currentXP + amount)
    
    -- Handle side effects explicitly
    CheckForLevelUp(player)
    UpdateAchievements(player) 
    LogXPGain(player, amount)
end

This approach is:

  • More performant (only runs when needed)
  • More predictable than event listeners

If you absolutely need change detection, you could implement a custom wrapper:

local function SetPathWithCallback(player, path, value, callback)
    PlayerState.SetPath(player, path, value)
    if callback then callback(value) end
end

But generally, explicit function calls are the better server-side pattern.

this module is amazing btw y’all gotta try ts

and probably a dumb question, does :AddToArray trigger .OnChanged? i can’t get it to trigger when i add an item to my inventory array