DataSafe
Production-Ready DataStore Manager
Session Locking | Auto-Backup | Versioning | Studio Mock Mode
Hey!
so you know that feeling when a player loses all their progress and blames you? or when a duplication glitch ruins your economy?
Yeah, DataStores are a pain ![]()
-- everyone's nightmare:
local success, data = pcall(function()
return datastore:GetAsync(key)
end)
if not success then
-- uh oh, what now?
-- retry? how many times?
-- what if the player dupes?
-- what about backups?
-- 💀💀💀
end
What is DataSafe?
I built a production-ready DataStore wrapper that actually handles all the scary stuff:
| Feature | Description |
Quick Stats
Before/After Comparison
❌ Before (manual DataStore handling)
local dss = game:GetService("DataStoreService")
local store = dss:GetDataStore("PlayerData")
-- load with retry logic
local function load_data(plr)
local key = "player_" .. plr.UserId
-- check if already loaded (duplication prevention)
if sessions[key] then
return nil -- or kick? or wait?
end
-- retry loop
local attempts = 0
while attempts < 3 do
local success, data = pcall(function()
return store:GetAsync(key)
end)
if success then
sessions[key] = true
return data or default_data
end
attempts += 1
task.wait(1 * attempts) -- exponential backoff
end
-- failed after retries... now what?
return nil
end
-- save with backup
local function save_data(plr, data)
-- backup to backup_1
pcall(function()
backup_store:SetAsync(key .. "_backup_1", data)
end)
-- actual save
local success = pcall(function()
store:SetAsync(key, data)
end)
-- what if this fails?
-- what about versioning?
-- did we unlock the session?
end
-- you get the idea... 50+ lines per game
âś… After (DataSafe)
local ds = require(rs._packages.datasafe)
ds.cfg({
mock_in_studio = true, -- uses studio_ prefix for DataStores
backup_keys = 3,
max_retries = 3,
version = 1
})
-- load (session lock automatic)
local result = ds.load("player_data", key, default_data)
if result.ok then
print("loaded:", result.data)
end
-- save (backup automatic)
ds.save("player_data", key, data)
-- close (saves + unlocks session)
ds.close("player_data", key)
-- that's it. done.
Session Locking (Anti-Dupe)
Prevents the same player data from being loaded twice (duplication glitch protection)
Show code example
-- automatic session locking
local result = ds.load("player_data", key)
if not result.ok and result.err == "session_locked" then
-- already loaded elsewhere, can't dupe
plr:Kick("session already active")
end
-- session unlocks on close
ds.close("player_data", key)
How it works:
• Locks when you load
• Blocks duplicate loads
• Auto-expires after 5 minutes (handles crashes)
• Unlocks when you close
No more dupe exploits ![]()
Auto-Backup System
Every save creates 3 backup copies automatically (configurable)
Show backup system
ds.cfg({
backup_keys = 3 -- keeps 3 backup copies
})
-- save creates backups automatically
ds.save("player_data", key, data)
-- creates: key_backup_1, key_backup_2, key_backup_3
-- rollback to any backup
ds.rollback("player_data", key, 1) -- restore backup 1
ds.rollback("player_data", key, 2) -- restore backup 2
Backup Rotation:
•backup_1= most recent
•backup_2= 2nd most recent
•backup_3= 3rd most recent
• Automatically rotates on each save
Never lose data again ![]()
Versioning & Migrations
Safely update your data structure without breaking old saves
Show migrations
ds.cfg({
version = 2,
migrations = {
[1] = function(data)
-- v0 -> v1: add inventory
data.inventory = data.inventory or {}
return data
end,
[2] = function(data)
-- v1 -> v2: restructure stats
data.stats = {
level = data.level or 1,
xp = data.xp or 0
}
data.level = nil
data.xp = nil
return data
end
}
})
-- old saves automatically migrate on load
local result = ds.load("player_data", key)
-- data is now version 2 regardless of when it was saved
Perfect for:
• Adding new fields
• Restructuring data
• Fixing old bugs
• Gradual updates
No more “data incompatible” errors ![]()
Studio Mock Mode
This is huge: test your game in Studio with real DataStores (uses studio_ prefix to keep it separate from production)
Show mock mode
ds.cfg({
mock_in_studio = true -- enable mock mode
})
-- in Studio:
ds.load("player_data", key) -- uses DataStore: studio_player_data
ds.save("player_data", key, data) -- saves to: studio_player_data
-- in actual game:
ds.load("player_data", key) -- uses DataStore: player_data
ds.save("player_data", key, data) -- saves to: player_data
-- check which mode you're in
print(ds.is_mock_mode()) -- true in Studio, false in game
Benefits:
• Test without Studio API limits issues
• Data persists between test sessions
• Zero risk to production data
• No conflicts between Studio and live game
• Automatic mode switching
Literally never accidentally corrupt real data while testing ![]()
Auto-Retry & Rate Limit Handling
Handles DataStore errors and rate limits automatically
Show retry config
ds.cfg({
max_retries = 3,
retry_delay = 1 -- exponential backoff
})
-- automatically retries on failure:
-- attempt 1: immediate
-- attempt 2: wait 1s
-- attempt 3: wait 2s
-- attempt 4: wait 3s
local result = ds.load("player_data", key)
if not result.ok then
-- only fails after all retries exhausted
warn("load failed:", result.err)
end
Handles:
• Network errors
• 502/503 errors (Roblox outages)
• Rate limits (429)
• Temporary outages
You don’t have to think about retries anymore ![]()
Update Transactions
Atomic updates using UpdateAsync under the hood
Show update example
-- safely modify data
local result = ds.update("player_data", key, function(data)
data.coins += 100
data.stats.xp += 50
return data
end)
if result.ok then
print("updated:", result.data)
end
-- prevents race conditions
-- uses UpdateAsync internally
Get it from the Creator Marketplace ![]()
Quick Start:
- Get it from the toolbox in Roblox Studio
- Drop it into
ReplicatedStorage - Require and use:
local ds = require(game.ReplicatedStorage._packages.datasafe)
ds.cfg({mock_in_studio = true})
-- that's it, you're protected
Zero dependencies, works immediately
Complete player data system - click to expand
local ds = require(rs._packages.datasafe)
local plrs = game:GetService("Players")
-- configure
ds.cfg({
mock_in_studio = true,
backup_keys = 3,
max_retries = 3,
auto_save = 60,
version = 2,
migrations = {
[1] = function(data)
data.inventory = data.inventory or {}
return data
end,
[2] = function(data)
data.stats = {level = 1, xp = 0}
return data
end
}
})
local default_data = {
coins = 0,
inventory = {},
stats = {level = 1, xp = 0}
}
-- load on join
plrs.PlayerAdded:Connect(function(plr)
local key = "player_" .. plr.UserId
local result = ds.load("player_data", key, default_data)
if result.ok then
for stat, val in result.data.stats do
plr:SetAttribute(stat, val)
end
else
plr:Kick("failed to load data")
end
end)
-- save on leave
plrs.PlayerRemoving:Connect(function(plr)
local key = "player_" .. plr.UserId
local cached = ds.get_cached(key)
if cached then
cached.stats.level = plr:GetAttribute("level")
cached.stats.xp = plr:GetAttribute("xp")
ds.close("player_data", key)
end
end)
-- auto-save every 60 seconds
task.spawn(function()
while task.wait(60) do
for _, plr in plrs:GetPlayers() do
local key = "player_" .. plr.UserId
local cached = ds.get_cached(key)
if cached then
cached.stats.level = plr:GetAttribute("level")
cached.stats.xp = plr:GetAttribute("xp")
ds.save("player_data", key, cached)
end
end
end
end)
Honestly? Peace of mind
DataStores are scary. One wrong retry and you lose player data. One missing lock and you get dupe exploits. One rate limit and players can’t join.
This handles all of it so you can focus on making your game instead of debugging data loss reports
| âś… Free forever (MIT license) | âś… Open source |
| âś… Battle-tested | âś… Zero dependencies |
| âś… Works with existing projects | |
- hell yeah, data loss was killing me
- probably gonna try it
- looks solid, might use later
- I’ll stick with my own system
- already using it and it’s clean
Made by @Snow_o29
If this saved your data, drop a
so others can find it!
Thanks for checking out DataSafe!