💾 Add Persistence
Save and load player inventories using ProfileStore or DataStore. Simple patterns for beginner to intermediate developers.
Overview
The three steps of persistence
Load
Fetch saved inventory when player joins
ProfileStore:LoadProfile()
→
Replace
Apply loaded data to InventoryState
state.Items = loadedData
→
Save
Store inventory when player leaves
ProfileStore:Save()
Option 1: ProfileStore (Recommended)
Use the ProfileStore module for easy data management
Setup ProfileStore
Luau
-- ServerScriptService/Persistence.luau
local ProfileService = require(game:GetService("ServerScriptService").ProfileStore)
local InventoryService = require(game:GetService("ReplicatedStorage"):WaitForChild("Shared"):WaitForChild("StowayServerV1_2"))
-- Create ProfileStore key
local PlayerData = ProfileService.GetProfileStore("PlayerData", {
Template = {
Items = {}, -- UUID -> Item data
ItemsByID = {}, -- ItemId -> UUID array
Hotbar = {}, -- Slot -> UUID
Storage = {}, -- UUID array
EquippedItemUUID = nil,
Weight = 0,
Settings = {
Limit = 15,
CanStack = true,
MaxStackSize = 5,
MaxHotbarSlots = 10,
Droppable = true,
UiType = "Default"
}
}
})
-- STEP 1: Load profile on player join
local OnPlayerAdded = function(player)
local profile = PlayerData.LoadProfileAsync("Player_" .. player.UserId)
if profile then
profile.AddUserId(player.UserId) -- Prevent concurrent writes
profile.Release() -- Allow player to load into game
-- Get or create inventory state
local state = InventoryService.GetState(player)
if state then
-- STEP 2: Replace state with loaded data
if profile.Data and profile.Data.Items then
state.Items = profile.Data.Items
state.ItemsByID = profile.Data.ItemsByID or {}
state.Hotbar = profile.Data.Hotbar or {}
state.Storage = profile.Data.Storage or {}
state.EquippedItemUUID = profile.Data.EquippedItemUUID
state.Weight = profile.Data.Weight or 0
-- Apply saved settings or use defaults
if profile.Data.Settings then
for k, v in pairs(profile.Data.Settings) do
state.Settings[k] = v
end
end
-- Sync to client
InventoryService.SyncSettings(player)
end
end
else
warn("Failed to load profile for", player.Name)
end
end
-- STEP 3: Save on player leave
local OnPlayerRemoving = function(player)
local profile = PlayerData.FindProfileByUserId("Player_" .. player.UserId)
if profile then
-- Get latest state before saving
local state = InventoryService.GetState(player)
if state then
profile.Data.Items = state.Items
profile.Data.ItemsByID = state.ItemsByID
profile.Data.Hotbar = state.Hotbar
profile.Data.Storage = state.Storage
profile.Data.EquippedItemUUID = state.EquippedItemUUID
profile.Data.Weight = state.Weight
profile.Data.Settings = state.Settings
end
-- Save and release
profile:SaveAsync()
profile:Release()
end
end
game:GetService("Players").PlayerAdded:Connect(OnPlayerAdded)
game:GetService("Players").PlayerRemoving:Connect(OnPlayerRemoving)
💡 Why ProfileStore?
- Automatic profile locking prevents data corruption
- Built-in session locking (players can't join full server)
- Handles DataStore quotas and rate limits
- Simple template system for data structure
Option 2: DataStore API (No External Module)
Manual implementation using Roblox DataStoreService
Manual DataStore Implementation
Luau
-- ServerScriptService/Persistence.luau
local DataStoreService = game:GetService("DataStoreService")
local InventoryStore = DataStoreService:GetDataStore("InventoryStore")
local InventoryService = require(game:GetService("ReplicatedStorage"):WaitForChild("Shared"):WaitForChild("StowayServerV1_2"))
local SavePlayerData = function(player)
local state = InventoryService.GetState(player)
if not state then return end
local success, err = pcall(function()
InventoryStore:SetAsync(
"Player_" .. player.UserId,
{
Items = state.Items,
ItemsByID = state.ItemsByID,
Hotbar = state.Hotbar,
Storage = state.Storage,
EquippedItemUUID = state.EquippedItemUUID,
Weight = state.Weight,
Settings = state.Settings
}
)
end)
if not success then
warn("Failed to save inventory for", player.Name, err)
end
end
local LoadPlayerData = function(player)
local success, data = pcall(function()
return InventoryStore:GetAsync("Player_" .. player.UserId)
end)
if success and data then
local state = InventoryService.GetState(player)
if state then
state.Items = data.Items or {}
state.ItemsByID = data.ItemsByID or {}
state.Hotbar = data.Hotbar or {}
state.Storage = data.Storage or {}
state.EquippedItemUUID = data.EquippedItemUUID
state.Weight = data.Weight or 0
if data.Settings then
for k, v in pairs(data.Settings) do
state.Settings[k] = v
end
end
end
end
end
-- Connect to player lifecycle
game:GetService("Players").PlayerAdded:Connect(function(player)
task.wait(2) -- Wait for inventory to initialize
LoadPlayerData(player)
end)
game:GetService("Players").PlayerRemoving:Connect(function(player)
SavePlayerData(player)
end)
-- Auto-save every 60 seconds
task.spawn(function()
while true do
task.wait(60)
for _, player in ipairs(game:GetService("Players"):GetPlayers()) do
if player and player:IsA("Player") then
task.spawn(SavePlayerData, player)
end
end
end
end)
What to Save
Only save these fields from InventoryState
When saving player inventory data, only persist the essential state fields. The inventory system manages runtime data like player references internally.
| Field | Type | Description |
|---|---|---|
Items |
table | UUID -> ItemInstance mapping |
ItemsByID |
table | ItemId -> UUID array (for stacking) |
Hotbar |
table | Slot number -> UUID |
Storage |
table | Array of UUIDs |
EquippedItemUUID |
string? | Currently equipped item |
Weight |
number | Total item count |
Settings |
table | Player preferences (vip limits, etc) |
⚠ Don't Save:
Player reference, die state, or any runtime-only data.
Best Practices
Tips for reliable data persistence
Call LoadProfile in PlayerAdded before any inventory operations.
Players may crash or lose connection. Auto-save every 30-60 seconds.
Manual DataStore is fine for testing, but ProfileStore handles edge cases.
Check that required fields exist before applying to state.
If save fails, log it but don't crash the server.
Next Steps
Related documentation