Migrating from ProfileStore
KeepBlox reads and writes the same records as ProfileStore and speaks its lock protocol. Your keys stay where they are, in the same format. Nothing is converted, and nothing needs converting back.
Change one require
Section titled “Change one require”KeepBlox.Compat.ProfileStore is ProfileStore’s API over KeepBlox. Replace the require and leave the
rest of the game as it is:
-- Before:-- local ProfileStore = require(ServerScriptService.ProfileStore)local ProfileStore = require(ServerScriptService.KeepBlox.Compat.ProfileStore)
local PlayerStore = ProfileStore.New("PlayerStore", TEMPLATE)
Players.PlayerAdded:Connect(function(player) local profile = PlayerStore:StartSessionAsync(`{player.UserId}`, { Cancel = function() return player.Parent ~= Players end, }) -- ... the rest of your ProfileStore code, unchangedend)The shim covers New, StartSessionAsync, GetAsync, RemoveAsync, MessageAsync, VersionQuery,
Mock, SetConstant, IsClosing, IsCriticalState, OnError, OnOverwrite and OnCriticalToggle,
and on profiles Data, LastSavedData, EndSession, Save, Reconcile, IsActive, AddUserId,
RemoveUserId, MessageHandler, SetAsync (on view profiles), OnSave, OnLastSave,
OnSessionEnd and OnAfterSave.
A spec runs game code written for ProfileStore through every simulator scenario on the shim
(tests/unit/CompatProfileStore.luau).
Where KeepBlox deliberately differs
Section titled “Where KeepBlox deliberately differs”The shim never imitates a ProfileStore bug. Where KeepBlox’s guarantee and ProfileStore’s behaviour
disagree, the guarantee wins (src/Compat/ProfileStore.luau lists each case):
- Foreign values are quarantined. A key holding something that is not a profile is copied to
<store>__quarantine, andStartSessionAsyncreturns nil. It is not overwritten with the template.OnOverwritenever fires;OnErrorreports it. - Bad data never fails every save. A value a data store cannot hold (bad UTF-8, a cycle, a function)
is repaired in what is stored and reported through
OnErrorwith its path; NaN and infinities are left out the same way. Data that cannot be repaired (too large, a mixed table, an array with holes, number keys in a dictionary) is refused with its path. In ProfileStore, a value the store cannot encode fails every save silently, and a mixed or sparse table is saved cut short without a word. - Ended sessions freeze their data. When a session ends,
Datais frozen. Late writes error instead of being lost quietly. - Saving follows KeepBlox’s schedule.
AUTO_SAVE_PERIOD,LOAD_REPEAT_PERIODandFIRST_LOAD_REPEATare accepted bySetConstantbut have no effect. KeepBlox writes changed data at each renewal and snapshots it in MemoryStore every beat (How it works). KeyInfois nil.MessageHandlerworks on active profiles only, not on view profiles.- A full message queue refuses.
MessageAsyncreturns false instead of dropping the oldest message.
SESSION_STEAL, ASSUME_DEAD and START_SESSION_TIMEOUT map to KeepBlox’s profileStoreSteal,
profileStoreDead and loadTimeout. The CRITICAL_STATE_* and MAX_MESSAGE_QUEUE constants are
accepted and have no effect.
Rolling out with mixed servers
Section titled “Rolling out with mixed servers”Old servers keep running ProfileStore while new servers run KeepBlox. A player moving between them is handed over like any other move:
- A KeepBlox server asking for a key a ProfileStore server holds sends ProfileStore’s own hand-over
message and waits by ProfileStore’s rules: it takes the key
profileStoreSteal(40 s) after its first request, or at once when the owner’sLastUpdateisprofileStoreDead(630 s) old. - A ProfileStore server asking for a key a KeepBlox server holds gets it the same way it would from another ProfileStore server.
- Offline messages cross between the libraries in both directions, and each is handled once.
- ProfileStore keeps KeepBlox’s
MetaData.KeepBloxwhen it rewrites a key.
These are proven by the specs in tests/scenario/Compat.luau, including “Compat: a rolling migration
with crashes keeps every invariant”.
- Ship the version with the new require. Use “Migrate to latest update” in the Creator Hub, or let old servers close on their own.
- Watch
OnError(orstore.onErroron the native API) during the rollout. - Keep compression off until every server runs KeepBlox (see below).
Switching back
Section titled “Switching back”Put the old require back and ship. Every key KeepBlox wrote is still a ProfileStore record, and the two libraries can again run side by side while the old version rolls out.
Two features break this, because ProfileStore cannot read what they write:
- Compression (
compresson a native store) stores large profiles as a compressed buffer. Turn it on only once every server runs KeepBlox. KeepBlox reads both forms, so you can turn it off again later, and each profile is stored plain at its next save. - Schema versions are kept in
MetaData.KeepBlox.schemaVersion. ProfileStore ignores them, so its servers see the migrated data, not the old shape.
ProfileService records
Section titled “ProfileService records”ProfileService stored the same record shape, and KeepBlox reads it as it is:
- Keys written by ProfileService load as they are. No importer is needed.
- A ProfileService-era lock (a session with no GUID) is honoured and taken over by ProfileStore’s rules. One that is long dead is taken over at once.
- ProfileService’s older message entries,
{index, version, locked, message}, are read by their last element.
The native API reads them too: open a store with the same name and a template.
Moving to the native API
Section titled “Moving to the native API”The shim is a translation layer. When you are ready, the native API gives you typed load results, end reasons, trades, receipts and schemas. It uses the same keys and records, so you can switch one store at a time, and servers on the shim and on the native API can run side by side.
| ProfileStore | KeepBlox |
|---|---|
ProfileStore.New(name, template) |
KeepBlox.store(name, { template = template }) |
store:StartSessionAsync(key, { Cancel, Steal }) returns a profile or nil |
store:load(key, { cancel, steal }) returns { ok, profile } or { ok = false, reason } |
profile:EndSession() |
profile:release() |
profile:Save() |
profile:save() |
profile:Reconcile() |
automatic on every load (reconcile, on by default) |
profile:IsActive() |
profile:isActive() |
profile:AddUserId(id) / RemoveUserId(id) |
profile:addUserId(id) / removeUserId(id) |
profile.OnSessionEnd |
profile.onEnded, with the reason |
profile.OnSave / OnLastSave |
profile.onSaving, with final and the reason |
profile.OnAfterSave |
profile.onSaved |
store:MessageAsync(key, message) |
store:message(key, message) |
profile:MessageHandler(fn) |
profile:onMessage(fn) |
store:VersionQuery(...) |
store:versions(key, query), store:readVersion, store:restore |
store:GetAsync(key) and SetAsync |
store:edit(key, fn) |
ProfileStore.OnError |
store.onError |
Two things change in the code, not just the names:
store:loadreturns a result. Checkresult.ok, and useresult.reasonin the kick message.- ProfileStore fills missing keys only when you call
Reconcile. The native store does it on every load; passreconcile = falseto keep ProfileStore’s behaviour.
See the quick start for a complete native setup.