💾 Add Persistence

Save and load player inventories using ProfileStore or DataStore. Simple patterns for beginner to intermediate developers.

Overview

The three steps of persistence

1

Load

Fetch saved inventory when player joins

ProfileStore:LoadProfile()
2

Replace

Apply loaded data to InventoryState

state.Items = loadedData
3

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

  • Load before player interacts with inventory
    Call LoadProfile in PlayerAdded before any inventory operations.
  • Save on both leave AND auto-save
    Players may crash or lose connection. Auto-save every 30-60 seconds.
  • Use ProfileStore for production
    Manual DataStore is fine for testing, but ProfileStore handles edge cases.
  • Validate loaded data
    Check that required fields exist before applying to state.
  • Handle errors gracefully
    If save fails, log it but don't crash the server.
  • Next Steps

    Related documentation