ProfileStore - Save your player data easy (DataStore Module)

“ProfileStore” by loleris

(Successor module to ProfileService)

[GitHub repo]

ProfileStore is a Roblox DataStore wrapper that streamlines auto-saving, session locking and a few other features for the game developer. ProfileStore’s source code runs on a single ModuleScript.

Read documentation here:
ProfileStore wiki (Click me)

Get the module here:
Roblox library (Click me)

(If you make a tutorial for this module, please contact me and I might share the link here!)

:moneybag::moneybag::moneybag:
Consider donating R$ to the creator of ProfileStore (Click here) if you find this resource helpful!


How does it work?

ProfileStore loads and caches data from a DataStore key on a single Roblox game server and prevents other game servers from accessing this data too soon by establishing a session lock and handling session lock conflicts between servers swiftly all while not using too many DataStore and MessagingService API calls.

Data units saved by ProfileStore are called “profiles” which can be accessed in-game by starting a “session”. During an active session you gain access to a table (Profile.Data) which will either be saved to the DataStore on the next auto-save or when you manually end the session.

ProfileStore is primarily player-data-oriented and, by design, tweaked for a common use case where each game player would have a single profile dedicated to storing their game progress. Session locking addresses the issue of data access from more than one game server (which can cause item “dupes” in games with trading) by keeping track of which game server is currently caching data and gracefully switches ownership from one server to the other without failing new session requests. ProfileStore can still be used for non-player data storage, although ProfileStore’s session locking is not ideal for quick writing from several game servers.

ProfileStore’s module functions try to resemble the Roblox API for a sense of familiarity to Roblox developers. Methods with the Async keyword yield until a result is ready (e.g. :StartSessionAsync()), while others do not.

ProfileStore is not designed (and never will be) for in-game leaderboards or any kind of global state.

Changes from ProfileService

ProfileStore is a successor to ProfileService - it uses a very similar mechanism for handling session locks which has been improved to be more responsive at handling conflicts between servers. Here’s a list of significant changes:

  • Default auto-save period increased from 30 to 300 seconds - Nearly x10 fewer DataStore calls consume less server resources which means more scalability! ProfileStore relies on auto-saves to store latest data and resolve session conflicts in a single :UpdateAsync() call. With the addition of MessagingService, ProfileStore can now auto-save slower while still reacting to external game servers trying to take the session lock. Under normal circumstances ProfileStore should outperform ProfileService in session conflict resolution time!

  • More performance, more server-friendly - MessagingService
    helps resolve session conflicts much faster. ProfileStore tries to strain Roblox services less when things inevitably do go wrong with exponential backoff, timeouts and cancel conditions.

  • Outdated 7 second DataStore queue replaced - An internal DataStore API call queue is needed to ensure calls are satisfied in order. Roblox DataStores have changed since ProfileService was released and the 7 second queue was replaced with a queue that performs calls to the same DataStore key as soon as all previous calls finish.

  • Luau types for autocompletion - This will help make fewer typos while writing code with ProfileStore.

  • API cleanup - Function and variable names have been changed to be shorter and more conventional.

  • MetaTags removed in favor of Profile.LastSavedData - MetaTags has been a piece of data exclusively used to verify data that has been successfully saved to the DataStore. Profile.LastSavedData will also satisfy this purpose - every time Profile.Data is saved to the DataStore, Profile.LastSavedData will be updated with the version of Profile.Data that has been successfully saved to the DataStore.

  • New profile messaging system replacing GlobalUpdates - GlobalUpdates was a complicated system for writing to profiles regardless of whether a server is currently running a session for them. ProfileStore:MessageAsync() is much easier to use and has fast delivery time by utilizing MessagingService. Use this for features like in-game player gifting where data delivery is crucial.

  • Profile.OnSave, Profile.OnLastSave and Profile.OnAfterSave signals - Useful for altering and reacting to data along ProfileStore’s DataStore requests.

Should I switch from ProfileService (the older module)?

ProfileStore hasn’t been used a lot in production yet, but has been thoroughly tested by similar tools that allowed ProfileService to stay mostly bug-free. Use this at your own risk and forward any bugs to the creator of this module - we’ll try to fix bugs super quickly!

It might be a good idea to let old projects keep using ProfileService and start using ProfileStore for brand new ones, but if you’re feeling risky…

ProfileStore DataStore profiles are backwards-compatible with ProfileService! ProfileService profiles should load from the DataStore using the same keys in ProfileStore without issue, but ProfileService (the older module) might have issues loading the same profiles again if you start using ProfileStore:MessageAsync() (on the new module). You should first do Roblox studio tests with API access before pushing this change live.


Example code:

local ProfileStore = require(game.ServerScriptService.ProfileStore)

-- The PROFILE_TEMPLATE table is what new profile "Profile.Data" will default to:
local PROFILE_TEMPLATE = {
   Cash = 0,
   Items = {},
}

local Players = game:GetService("Players")

local PlayerStore = ProfileStore.New("PlayerStore", PROFILE_TEMPLATE)
local Profiles: {[player]: typeof(PlayerStore:StartSessionAsync())} = {}

local function PlayerAdded(player)

   -- Start a profile session for this player's data:

   local profile = PlayerStore:StartSessionAsync(`{player.UserId}`, {
      Cancel = function()
         return player.Parent ~= Players
      end,
   })

   -- Handling new profile session or failure to start it:

   if profile ~= nil then

      profile:AddUserId(player.UserId) -- GDPR compliance
      profile:Reconcile() -- Fill in missing variables from PROFILE_TEMPLATE (optional)

      profile.OnSessionEnd:Connect(function()
         Profiles[player] = nil
         player:Kick(`Profile session end - Please rejoin`)
      end)

      if player.Parent == Players then
         Profiles[player] = profile
         print(`Profile loaded for {player.DisplayName}!`)
         -- EXAMPLE: Grant the player 100 coins for joining:
         profile.Data.Cash += 100
         -- You should set "Cash" in PROFILE_TEMPLATE and use "Profile:Reconcile()",
         -- otherwise you'll have to check whether "Data.Cash" is not nil
      else
         -- The player has left before the profile session started
         profile:EndSession()
      end

   else
      -- This condition should only happen when the Roblox server is shutting down
      player:Kick(`Profile load fail - Please rejoin`)
   end

end

-- In case Players have joined the server earlier than this script ran:
for _, player in Players:GetPlayers() do
   task.spawn(PlayerAdded, player)
end

Players.PlayerAdded:Connect(PlayerAdded)

Players.PlayerRemoving:Connect(function(player)
   local profile = Profiles[player]
   if profile ~= nil then
      profile:EndSession()
   end
end)

Other resources:

Check out Replica - A server to client state replication solution - Can be useful in combination with ProfileStore!

217 Likes

Consider donating R$ to the creator of ProfileStore if you find this resource helpful!

Your support helps me make more epic open source code :sunglasses: :+1:
Of course, ProfileStore is completely free to use! :eyes:

26 Likes

Super awesome and can’t wait to play around with this + migrate existing projects to ProfileStore! ProfileService had been the data backbone of my projects due to its reliability and ‘set-up and forget’ ease of use.

Are there any plans to make adjustments or improvements to ReplicaService? I know ReplicaService previously was designed to handle the data replication with this (ProfileService previously) in mind + with backwards-compatibility I wouldn’t expect any major hurdles, but just curious if that will have any reworking in the future.

9 Likes

The new module “Replica” will be released in a few days! It’s already finished, but I need to write the documentation.

19 Likes

What made you decide on this approach? Wouldn’t this effectively cut the max storage per key in half?

Maybe most projects won’t be heavily affected by this but I imagine that any experiences that have building or similar high-storage requirements will. Is the tradeoff worth it?

8 Likes

How does this compare to Suphi’s DataStore Module?

16 Likes

Loleris new profileservice? Can’t wait to use it!
Published it to Wally if anyone needs it there: Wally

8 Likes

I ain’t even read it yet and I know this fire good work

9 Likes

Really happy that this ended up being released! I like a lot of the improvements here, OnSave and OnLastSave in particular seem interesting. GlobalUpdates being simplified is also a huge win.

I also cant help but notice that ListenToHopReady does not exist anymore. Does this mean we can freely teleport players without having to worry about the profile at all?

7 Likes

It took a couple of minutes, but I managed to convert my game from ProfileService to ProfileStore. Things seem to be working, but I’m not sure if I want to publish this to production. I think for now I will experiment with it in our staging branch where players can find any issues.

This is pretty awesome, ProfileService (& now ProfileStore) has been a massive help for our game, thanks a ton for all of your work!

9 Likes

Profile.LastSavedData is a server-side cache of the last saved version. It does not take up storage in the DataStore.

6 Likes

I will wait for any feedback on profile loading between server teleports - I personally do not use ListenToHopReady in my own game between teleports and have not ran into any issues.

8 Likes

I appreciate the quick early adoption! Will await your further feedback :+1:

6 Likes

this is fire :fire: thanks loleris!

6 Likes

Awesome seeing a sequel to an already popular datastore module.

To make setup easier, do you think you could publish and maintain this on Wally?

8 Likes

I’m not familiar with Wally, but I’ve tried publishing ProfileStore to Wally. I would be grateful if you could reach out to me personally and let me know whether the current Wally setup is good :slightly_smiling_face:

7 Likes

how does this compare to document service?

3 Likes

Would there be a function to yield until profile is Saved & return if its successfully saved or not?

3 Likes

Looks Good! :fire: I will use it after the risks are gone.
Can’t wait for new Replica :slightly_smiling_face:

3 Likes

This looks fantastic! Looking forward to trying this out over ProfileService

3 Likes