Quick start
This page builds the usual setup: one store, a profile loaded when a player joins, released when they
leave. It assumes KeepBlox is a ModuleScript in ServerScriptService
(Installation).
The whole script
Section titled “The whole script”-- ServerScriptService/PlayerData.server.luaulocal Players = game:GetService("Players")local ServerScriptService = game:GetService("ServerScriptService")local KeepBlox = require(ServerScriptService.KeepBlox)
local store = KeepBlox.store("Profiles", { template = { coins = 0, inventory = {} },})
store.onError:connect(function(key, message) warn(`[KeepBlox] {key}: {message}`)end)
local function keyFor(player) return `u_{player.UserId}`end
local function onPlayerAdded(player) local result -- A timeout loses nothing: the data waits in the store, and nobody else can write it meanwhile. So -- load again while the player is still here, and kick only after a few tries. for _ = 1, 3 do result = store:load(keyFor(player), { -- Polled while the load waits: stop if the player left. cancel = function() return player.Parent == nil end, }) if result.ok or result.reason ~= "timeout" then break end end if not result.ok then if result.reason ~= "cancelled" then player:Kick(`Your data could not be loaded ({result.reason}). Please rejoin.`) end return end
local profile = result.profile profile:addUserId(player.UserId) -- for data erasure requests
profile.onEnded:connect(function(reason) -- "released": we released it. "shutdown": the server is closing. -- "handedOver" or "lost": another server has the profile now. if reason == "handedOver" or reason == "lost" then player:Kick("Your data was opened on another server.") end end)
if player.Parent == nil then -- The player left while the load finished. profile:release() return end
-- The game uses profile.Data from here on. profile.Data.coins += 10end
Players.PlayerAdded:Connect(onPlayerAdded)for _, player in Players:GetPlayers() do task.spawn(onPlayerAdded, player)end
Players.PlayerRemoving:Connect(function(player) local profile = store:profiles()[keyFor(player)] if profile then profile:release() endend)There is no BindToClose in this script: every store releases all of its open profiles in parallel
when the server shuts down, within shutdownDeadline (25 seconds by default).
The store
Section titled “The store”KeepBlox.store(name, options) opens one DataStore called name. It needs a template (the data a new
profile starts with) or a schema (Schema and migrations),
not both. The template is filled into loaded data deeply on every load: keys the stored data lacks get
a copy of the template’s value, and existing values are never replaced.
The store renews, saves and releases its profiles in the background. You do not call save on a timer.
Store reference lists every option.
Loading
Section titled “Loading”store:load(key, options) yields until the profile is yours or the load fails. It never throws for a
data store problem. It returns a result:
{ ok = true, profile = profile }, or{ ok = false, reason = reason }.
The reasons you are most likely to see:
| Reason | Meaning |
|---|---|
"cancelled" |
cancel() returned true: the player left. Nothing was kept. |
"timeout" |
The load did not finish within loadTimeout (120 seconds by default). Nothing is lost: load again while the player is still here. |
"closing" |
The server began shutting down. |
"open" |
This server already has a live session for that key. |
"foreign" |
The key holds something that is not a profile. It was copied to quarantine and left untouched. |
"outbid" |
A third server asked for the key after this one, and gets it. |
Every reason, including the schema and import ones, is on the load failures page. A failed load never writes data: a key taken and then given up (the player left, or shutdown began) is released without changing it.
cancel
Section titled “cancel”cancel is a function polled while the load waits. When it returns true, the load stops with
"cancelled", and a key that was already claimed is given back. Without it, a player who leaves during
a long wait still has their key claimed for a profile nobody uses.
When another server holds the key
Section titled “When another server holds the key”The load asks the other server to hand the profile over. A live KeepBlox server saves and releases within a second of the message, or at its next MemoryStore beat if messaging is down. A server that crashed is known dead within about 10 seconds. On Roblox’s real services a hand-over took 4-5 s in all, and taking over from a crashed server 12-13 s. How it works covers each case.
Using the data
Section titled “Using the data”profile.Data is a plain table. Change it in place:
profile.Data.coins += 100table.insert(profile.Data.inventory, { id = "sword", level = 1 })KeepBlox notices the change and saves it. Before every write it checks the data. A value a data store
cannot hold (a string that is not valid UTF-8, a cycle, a function, an Instance), and NaN or infinity
(which Roblox stores, but which break every sum they touch), is repaired in what is stored, so it never
costs the rest of the save, and store.onError names its path, for example Data.inventory[12].name.
What cannot be repaired is refused, and the last good save stays: a mixed table, an array with holes, a
dictionary with number keys, or more than 4 MB.
profile:save() saves at once and yields until the save is stored. It returns true, or false if it
could not be stored. You rarely need it; purchases and trades have their own helpers.
When a session ends
Section titled “When a session ends”profile.onEnded fires once, with the reason the session ended:
| Reason | What happened |
|---|---|
"released" |
The game called profile:release(). |
"shutdown" |
The server closed, and the store released every profile. |
"handedOver" |
Another server asked for the key, and this one saved and gave it up. |
"lost" |
A write found the key owned by another server. Nothing more was written. |
By the time onEnded fires, profile.Data is frozen with table.freeze, all the way down. Code that
still writes to it errors instead of changing a copy nobody will save. That is what stops a trade or a
purchase on a stale copy after the session moved elsewhere.
profile:isActive() is true while the session is live and not releasing.
Releasing
Section titled “Releasing”profile:release() ends the session: it makes a final save, then lets go of the lock. It yields until
done. The data freezes when the release begins, so the final save holds every change the game made.
Just before that, profile.onSaving fires with final = true: the last chance to write into Data.
A player who rejoins the same server while their release is still running does not get "open": the
new load waits for the release, then claims the key afresh.
Finding open profiles
Section titled “Finding open profiles”store:profiles() returns the profiles open on this server, by key. It is a new table on every call,
so looking one up is cheap and holding it is safe.
Errors
Section titled “Errors”store.onError fires with (key, message) for every problem worth logging: a failed call, refused
data, a quarantined value, a failed import read. The key is "" for a problem not tied to one key. Log
it; it is how bad data and outages become visible.
- Guarantees: what KeepBlox promises, and the spec behind each promise.
- Purchases: developer products granted exactly once.
- Profile reference: every field, method and signal.