DataStoreHandler is a type-safe Luau module for managing player profile data. It provides dot-notation path access, atomic batch operations, deep copy/merge/diff utilities, and seamless integration with ProfileService, ProfileStore, DataStore2, and standard DataStore APIs.
Key Features:
- Dot-notation paths (
"Stats.Health","Inventory.1.Name") - Atomic batch operations (all-or-nothing updates)
- Copy-on-write optimization
- Snapshot/Restore mechanics
- ProfileService, ProfileStore, DataStore, DataStore2 compatible
- Zero dependencies
Installation
- Download DataStoreHandler to your game
- Place it in
ServerScriptServiceor your modules folder - Require it in your scripts:
local DataStoreHandler = require(game.ServerScriptService.DataStoreHandler)
Quick Start Examples
Complete Example: Quest System
local Players = game:GetService("Players")
local DataStoreHandler = require(game.ServerScriptService.DataStoreHandler)
local PlayerHandlers = {}
Players.PlayerAdded:Connect(function(player)
local data = {
Quests = { Active = {}, Completed = {} },
Stats = { Level = 1 },
}
local handler = DataStoreHandler.New(data, player, false)
PlayerHandlers[player] = handler
-- Start a quest
local function questStart(questId, questName, objective)
handler:Set("Quests.Active." .. questId, {
Name = questName,
Objective = objective,
Progress = 0,
StartTime = os.time(),
})
end
-- Update quest progress
local function questProgress(questId, progress)
handler:Set("Quests.Active." .. questId .. ".Progress", progress)
print("Quests.Active." .. questId .. ".Progress", progress.." :", handler:GetRaw())
end
-- Complete quest
local function questComplete(questId)
local quest = handler:Get("Quests.Active." .. questId)
print("Quest found:", quest)
print("Active table:", handler:Get("Quests.Active"))
if quest then
local setOk = handler:Set("Quests.Completed." .. questId, quest)
local removeOk = handler:RemoveByKey("Quests.Active." .. questId)
print("Set ok:", setOk, "Remove ok:", removeOk)
print("GetRaw:", handler:GetRaw())
print("Player:", handler:GetPlayer())
return true
end
warn("Quest not found:", questId)
return false
end
-- Character spawn
player.CharacterAdded:Connect(function(character: Model)
-- Example usage
questStart("kill_10_goblins", "Goblin Slayer", "Kill 10 goblins")
questProgress("kill_10_goblins", 5)
questProgress("kill_10_goblins", 10)
questComplete("kill_10_goblins")
end)
end)
-- Cleanup (IMPORTANT to avoid memory leaks)
Players.PlayerRemoving:Connect(function(player)
PlayerHandlers[player]:Destroy()
PlayerHandlers[player] = nil
end)
Example 1: Basic Setup with ProfileService
local Players = game:GetService("Players")
local DataStoreHandler = require(game.ServerScriptService.DataStoreHandler)
local ProfileService = require(game.ServerScriptService.ProfileService)
local STORE_NAME = "PlayerProfiles"
local PROFILE_TEMPLATE = {
Stats = { Health = 100, Level = 1, Experience = 0 },
Inventory = {},
Currency = { Coins = 0, Gems = 0 },
}
local ProfileStore = ProfileService.GetProfileStore(STORE_NAME, PROFILE_TEMPLATE)
local PlayerHandlers = {}
Players.PlayerAdded:Connect(function(player)
local profile = ProfileStore:LoadProfileAsync("Player_" .. player.UserId)
if not profile then
player:Kick("Failed to load profile")
return
end
profile:Reconcile()
profile:ListenToRelease(function()
PlayerHandlers[player] = nil
player:Kick("Profile released")
end)
-- Create handler (reference mode to sync with ProfileService)
local handler, success = DataStoreHandler.New(profile.Data, player, false)
if not success then
profile:Release()
player:Kick("Handler creation failed")
return
end
PlayerHandlers[player] = handler
print("Handler loaded for " .. player.Name)
end)
Players.PlayerRemoving:Connect(function(player)
local handler = PlayerHandlers[player]
if handler then
handler:Destroy()
end
end)
Example 2: Simple Data Access
local handler = PlayerHandlers[player]
-- Read data
local health = handler:Get("Stats.Health")
local coins = handler:Get("Currency.Coins")
local inventory = handler:Get("Inventory")
-- Write data
handler:Set("Stats.Health", 50)
handler:Set("Stats.Level", 5)
-- Check if exists
if handler:Has("Achievements.Secret") then
print("Secret achievement unlocked!")
end
Example 3: Numeric Operations
local handler = PlayerHandlers[player]
-- Increment values
handler:Increment("Currency.Coins", 100) -- Add 100 coins
handler:Increment("Stats.Experience", 50) -- Add 50 XP
-- Math operations
handler:OperateOnValue("Stats.Health", "-", 25) -- Take 25 damage
handler:OperateOnValue("Stats.Speed", "*", 1.5) -- Apply 50% speed buff
-- Toggle boolean
handler:Toggle("Settings.AudioEnabled") -- true ↔ false
Example 4: Inventory Management
local handler = PlayerHandlers[player]
-- Add item to inventory
local item = { Name = "Sword", Damage = 25, Rarity = "Rare" }
handler:AddToList("Inventory", item)
-- Find item by name
local index, foundItem = handler:FindInList("Inventory", function(i)
return i.Name == "Sword"
end)
if index then
print("Found " .. foundItem.Name .. " at index " .. index)
end
-- Update item
handler:UpdateList("Inventory." .. index, { Name = "Enchanted Sword", Damage = 50 })
-- Remove item
handler:RemoveByKey("Inventory." .. index)
-- Count inventory
local itemCount = handler:CountList("Inventory")
print("Items in inventory: " .. (itemCount or 0))
-- Clear inventory
handler:ClearList("Inventory")
Example 5: Batch Operations (Atomic Updates)
local handler = PlayerHandlers[player]
-- Level up player (all-or-nothing)
local success = handler:BatchSet({
["Stats.Level"] = handler:Get("Stats.Level") + 1,
["Stats.Experience"] = 0,
["Stats.HealthMax"] = handler:Get("Stats.HealthMax") + 10,
["Achievements.LevelReached." .. tostring(newLevel)] = true,
})
if success then
print("Level up successful!")
else
print("Level up failed - no changes applied")
end
Example 6: Snapshot & Restore (Undo System)
local handler = PlayerHandlers[player]
-- Take a backup before risky operation
local backup = handler:Snapshot()
-- Make changes
handler:Set("Stats.Health", 10)
handler:Increment("Currency.Coins", -1000)
-- Compare what changed
local changes = handler:Diff(backup)
for path, change in changes do
print(path .. ": " .. tostring(change.old) .. " → " .. tostring(change.new))
end
-- Undo all changes
if handler:Restore(backup) then
print("Changes reverted!")
end
Example 7: Partial Updates (Patch)
local handler = PlayerHandlers[player]
-- Only update Audio settings, keep other Settings intact
handler:Patch("Settings", {
Audio = {
Volume = 0.5,
Enabled = true,
}
})
-- Settings.Brightness and other keys remain unchanged
Example 8: Advanced - Quest System
local handler = PlayerHandlers[player]
-- Start a quest
local function startQuest(questId, questData)
local questPath = "Quests.Active." .. questId
local questInfo = {
Id = questId,
Name = questData.Name,
Progress = 0,
MaxProgress = questData.MaxProgress,
Rewards = questData.Rewards,
StartTime = os.time(),
}
return handler:Set(questPath, questInfo)
end
-- Update quest progress
local function updateQuestProgress(questId, progress)
return handler:Set("Quests.Active." .. questId .. ".Progress", progress)
end
-- Complete quest and grant rewards
local function completeQuest(questId)
local questPath = "Quests.Active." .. questId
local quest = handler:Get(questPath)
if not quest then return false end
-- Move to completed
handler:Set("Quests.Completed." .. questId, quest)
handler:RemoveByKey(questPath)
-- Grant rewards
if quest.Rewards then
for rewardType, amount in quest.Rewards do
handler:Increment("Currency." .. rewardType, amount)
end
end
return true
end
-- Usage
startQuest("kill_goblins", {
Name = "Kill 10 Goblins",
MaxProgress = 10,
Rewards = { Coins = 100, Experience = 500 },
})
updateQuestProgress("kill_goblins", 5)
updateQuestProgress("kill_goblins", 10)
completeQuest("kill_goblins")
Example 9: Performance - Compiled Paths
local handler = PlayerHandlers[player]
-- Pre-compile paths used frequently (zero string parsing overhead)
local PATH_COINS = { "Currency", "Coins" }
local PATH_LEVEL = { "Stats", "Level" }
local PATH_HEALTH = { "Stats", "Health" }
-- Daily reward (called many times per second)
local function grantDailyReward()
return handler:BatchSetCompiled({
[PATH_COINS] = (handler:Get("Currency.Coins") or 0) + 100,
[{ "Rewards", "LastDaily" }] = os.time(),
})
end
-- Combat system - no string parsing overhead
local function takeDamage(amount)
local currentHealth = handler:Get("Stats.Health") or 100
return handler:BatchSetCompiled({
[PATH_HEALTH] = math.max(0, currentHealth - amount),
})
end
Example 10: RPG Leveling System
local handler = PlayerHandlers[player]
local function levelUpPlayer()
local currentLevel = handler:Get("Stats.Level") or 1
local newLevel = currentLevel + 1
-- Atomic level up operation
local changes = {
["Stats.Level"] = newLevel,
["Stats.Experience"] = 0,
["Stats.HealthMax"] = 100 + (newLevel * 10),
["Stats.Health"] = 100 + (newLevel * 10),
["Stats.ManaMax"] = 50 + (newLevel * 5),
["Stats.Mana"] = 50 + (newLevel * 5),
["Achievements.LevelMilestones.Level" .. newLevel] = true,
}
if handler:BatchSet(changes) then
print("Level up to " .. newLevel)
return true
else
print("Level up failed")
return false
end
end
levelUpPlayer()
API Reference
API Reference
Reading Data
Get(path)- Get value at pathGetRaw()- Get entire data tableGetPlayer()- Get associated playerHas(path)- Check if path exists
Writing Data
Set(path, value)- Set value at pathReset(path, defaultValue)- Reset to defaultPatch(path, partialData)- Merge partial updates
Numeric Operations
Increment(path, amount)- Add to numberOperateOnValue(path, operation, operand)- Math operations (+, -, *, /, %)Toggle(path)- Flip boolean
Batch Operations
BatchSet(changes)- Atomic multi-setBatchSetCompiled(changes)- Atomic multi-set with compiled paths
List/Array Operations
AddToList(path, value)- Add to array/dictUpdateList(path, value)- Update elementRemoveByKey(path)- Remove elementRemoveByKeyList(path, keyList)- Remove multipleClearList(path)- Empty tablePopFromList(path, index?)- Remove and returnCountList(path)- Get element countFindInList(path, predicate)- Search arrayMapList(path, transform)- Transform all elementsMoveInList(path, fromIndex, toIndex)- Reorder
Snapshots & Restore
Snapshot()- Full backupSnapshotPath(path)- Backup one branchRestore(snapshot)- Restore from backupDiff(snapshot)- See what changed
Other
Swap(pathA, pathB)- Swap two valuesDestroy()- Cleanup handler
Integration Guides
DataStore
local DataStoreService = game:GetService("DataStoreService")
local playerStore = DataStoreService:GetDataStore("PlayerProfiles")
Players.PlayerAdded:Connect(function(player)
local success, data = pcall(function()
return playerStore:GetAsync("Player_" .. player.UserId)
end)
if not success or data == nil then
data = { Stats = { Level = 1, Health = 100 }, Inventory = {} }
end
local handler = DataStoreHandler.New(data, player, false)
PlayerHandlers[player] = handler
end)
Players.PlayerRemoving:Connect(function(player)
local handler = PlayerHandlers[player]
if handler then
playerStore:SetAsync("Player_" .. player.UserId, handler:GetRaw())
handler:Destroy()
end
end)
DataStore2
local DataStore2 = require(game.ServerScriptService.DataStore2)
DataStore2.Combine("PRIMARY", "Stats", "Inventory")
Players.PlayerAdded:Connect(function(player)
local primaryStore = DataStore2("PRIMARY", player)
local data = primaryStore:Get({
Stats = { Level = 1 },
Inventory = {},
})
local handler = DataStoreHandler.New(data, player, false)
PlayerHandlers[player] = handler
end)
ProfileService & ProfileStore
Performance Tips
Tips
-
Pre-compile paths for operations called frequently:
local PATH_COINS = { "Currency", "Coins" } handler:BatchSetCompiled({ [PATH_COINS] = 1000 }) -
Batch operations instead of individual calls:
-- ❌ Bad handler:Set("A", 1) handler:Set("B", 2) -- ✅ Good handler:BatchSet({ ["A"] = 1, ["B"] = 2 }) -
Snapshot only what you need:
-- ✅ Efficient local backup = handler:SnapshotPath("Inventory") -
Use reference mode for ProfileService (no copy):
-- ✅ Direct sync local handler = DataStoreHandler.New(profile.Data, player, false)
Benchmark
local DataStoreHandler = require(game.ServerScriptService.DataStoreHandler)
local function makeBigData(size)
local data = {
Stats = {
Level = 1,
XP = 0,
},
Inventory = {},
}
for i = 1, size do
data.Inventory[i] = {
Id = i,
Name = "Item" .. i,
Value = i * 10,
}
end
return data
end
local function benchmark(label, fn)
local memBefore = gcinfo()
local t0 = os.clock()
fn()
local dt = os.clock() - t0
local memAfter = gcinfo()
print(string.format(
"[%s] Time: %.4f ms | Memory: %.2f KB (approx)",
label,
dt * 1000,
memAfter - memBefore
))
end
local SIZE = 5000
local rawData = makeBigData(SIZE)
local handler = DataStoreHandler.New(rawData, nil, true)
-- TESTS
benchmark("Get x10000", function()
for i = 1, 10000 do
handler:Get("Stats.Level")
end
end)
benchmark("Set x5000", function()
for i = 1, 5000 do
handler:Set("Stats.Level", i)
end
end)
benchmark("BatchSet", function()
handler:BatchSet({
["Stats.Level"] = 50,
["Stats.XP"] = 999,
["Inventory.1.Value"] = 9999,
})
end)
local PATH_LEVEL = { "Stats", "Level" }
local PATH_XP = { "Stats", "XP" }
local PATH_ITEM = { "Inventory", 1, "Value" }
benchmark("BatchSetCompiled", function()
handler:BatchSetCompiled({
[PATH_LEVEL] = 50,
[PATH_XP] = 999,
[PATH_ITEM] = 9999,
})
end)
benchmark("Snapshot (deepCopy)", function()
handler:Snapshot()
end)
benchmark("Diff", function()
local snap = handler:Snapshot()
handler:Set("Stats.Level", 100)
handler:Diff(snap)
print(handler:GetRaw())
end)
Error Handling Example
Summary
local handler, success = DataStoreHandler.New(data, player)
if not success then
warn("Failed to create handler for " .. player.Name)
player:Kick()
return
end
-- Operations return boolean success flags
if not handler:Set("Stats.Level", 5) then
warn("Failed to set level")
end
if not handler:Increment("Currency.Coins", 100) then
warn("Failed to increment coins")
end
Documentation
GitHub Repository