Getting Started¶
Scribe is persistent, fully-typed, automatically-replicated player data for Roblox, built on ProfileStore. You declare your data shape once, and Scribe handles saving, session locking, and streaming it to clients. The whole accessor tree is typed end to end.
Install¶
With Wally:
ProfileStore is a dependency of Scribe, so wally install pulls it into ServerPackages/ automatically. You don't declare it yourself.
New Luau type solver
The typed accessor tree (calls like data.Coins.Increment(50), data.Settings.Music.Set(false), or data.Inventory.Sword.Get()) needs the new Luau type solver. In Studio, select Workspace in the Explorer and set its UseNewLuauTypeSolver property (under Scripting) to Enabled. In an external editor, enable the new solver in your Luau LSP settings. Scribe runs correctly without it; Data.Raw is the untyped escape hatch.
One shared module¶
Declare the template and options in a single ModuleScript that both the server and client require. It returns { Server, Client }.
-- ReplicatedStorage/Shared/Data.luau
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Scribe = require(ReplicatedStorage.Packages.Scribe)
type ItemData = { Health: number, Dmg: number }
local template = {
Coins = Scribe.Int(0, { Min = 0 }),
Gems = 0,
Equipped = nil :: string?,
Inventory = {} :: { [string]: ItemData },
Settings = { Music = true, Sfx = true },
}
return Scribe({
Template = template,
Transport = "Default",
ProfileStoreIndex = "PlayerData", -- required: your DataStore name
ProfileKeyPrefix = "PLAYER_", -- required: per-player key prefix
})
ProfileStoreIndex and ProfileKeyPrefix are required. Naming your store is deliberate so two datastores can never share the same storage by accident.
Server¶
Profiles load asynchronously, so wait for the data before touching it:
local Players = game:GetService("Players")
local Data = require(ReplicatedStorage.Shared.Data).Server
Players.PlayerAdded:Connect(function(player)
local data = Data.WaitForData(player)
if not data then return end -- load failed / timed out; player is being kicked
data.Coins.Increment(50)
data.Settings.Music.Set(false)
print(data.Inventory.Sword_001.Dmg.Get())
end)
Data[player] / Data.Get(player) error while a profile is still loading. Use them only after WaitForData.
Client¶
Read the local player's data with the same accessor API. Observe fires immediately and on every change:
local Data = require(ReplicatedStorage.Shared.Data).Client
Data.Coins.Observe(function(coins)
coinsLabel.Text = tostring(coins)
end)
The client must require the module too
Replication only starts when the bundle is required from a client-side script (a LocalScript). That require runs the client's handshake with the server. If you set Scribe up on the server but never require it on the client, the client never syncs: Observe/Changed never fire with real data (reads stay at your template defaults), GetShared is empty, and the server logs a CLIENT_HANDSHAKE_TIMEOUT warning ("client never sent Hello"). Requiring the same shared module on both realms is the whole setup. There is nothing else to wire up.
Unlike the server, the client never makes you wait. Reads return your template defaults until the first sync arrives, and Observe (and Changed) fire the moment real data lands, so reactive UI updates itself without any gating. For a one-off imperative read at startup, where the default would be wrong, check Data.IsReady() or yield on Data.WaitForData(timeout) first.
Client writes are local-only (optimistic UI). Server ops always win. To change data authoritatively, call a server command:
-- server
Data.Command("EquipItem", { Args = { "string" } }, function(player, itemId)
if not Data[player].Inventory[itemId].Get() then return false, "not owned" end
Data[player].Equipped.Set(itemId)
return true
end)
-- client
local ok, reason = Data.Request("EquipItem", "Sword_001")
Configuration¶
The options table is fully typed as ScribeOptions<T>, so your editor autocompletes field names and flags a wrong type or a misspelled key. Beyond the three required fields shown above, the ones you reach for first:
SaveInterval(number?): seconds between autosaves (default 300).Migrations({ [number]: (data) -> () }?): evolve your data shape across versions.UseMock/DontSave(boolean?): play-test without touching live data.Leaderboards,Products,Passes,Perks: opt into monetization and boards.
Every option, with its type and default, is in the Configuration reference.
Already handle Robux purchases yourself?
If you register Products, Scribe takes over MarketplaceService.ProcessReceipt to grant them (it leaves it alone for data-only games). If your game already runs its own receipt handler, set OwnReceipts = false and route Scribe's products through Data.HandleReceipt. See Monetization for details.
Where to next¶
- Configuration: every
Scribe({})option, typed, with defaults. - Templates & Declarators: how to shape your data template and pick field types.
- Replication & Visibility: who sees what (
ServerOnly,Shared,Session). - Session Lifecycle: loading, saving, and session end.
- Monetization & Gifting, Leaderboards, Diagnostics, Testing.
- The API reference for every method.