Visual DataStoring (Simple Store)

Easy Player Data Saving (SimpleStore v2.0)

Have you ever wanted to use DataStores but got lost in the confusing API calls, retries, or table serialization? Don’t worry — this simple DataHandler module takes care of everything for you.

It automatically saves, loads, and builds your player data structure just by giving it a single template folder. No more writing save functions or worrying about missing values — everything just works out of the box.


:brain: What It Does

Once installed, the system automatically:

  • Clones your DataTemplate into each player when they join
  • Loads their saved data from Roblox DataStore
  • Automatically saves data when they leave
  • Autosaves every 60 seconds (configurable)
  • Supports both normal folders and dynamic container folders (for things like Pets, Weapons, Characters, etc.)
  • Rebuilds missing folders or values if they ever get deleted
  • Has retry logic so failed saves don’t just vanish

All you have to do is set up your DataTemplate, require the module, and call DataHandler:Init().


:gear: Setup

  1. Create a folder in ServerStorage called DataTemplate.
    Inside it, make your value objects and subfolders exactly how you want them to appear in each player.
    Example:
DataTemplate
├── Stats
│   ├── Level (IntValue)
│   └── Gold (IntValue)
├── PetsFolder
│   ├── PetTemplate (Folder)
│   │   ├── Shiny (BoolValue)
│   │   ├── Equipped (BoolValue)
│   │   ├── Level (IntValue)
│   │   ├── EXP (IntValue)
│   │   └── PetName (StringValue)
│   └── (Attributes: IsContainer = true, TemplateName = "PetTemplate")
├── Characters
│   ├── CharacterTemplate (Folder)
│   │   ├── HP (IntValue)
│   │   ├── Stamina (IntValue)
│   │   ├── Name (StringValue)
│   │   └── EquippedWeapon (StringValue)
│   └── (Attributes: IsContainer = true, TemplateName = "CharacterTemplate")

Containers are special folders with the attribute IsContainer = true.
They can dynamically clone items (like Pets, Characters, etc.) from their internal template.


:laptop: Example Usage

Once DataHandler is initialized, it automatically handles all saving and loading — you just work with the player’s data like normal Roblox instances.

Here are some practical examples:

local DataHandler = require(game.ServerScriptService.DataHandler)
DataHandler:Init()

-- 🪙 Add gold to a player
local playerData = game.Players.["Player Name"].PlayerData
local stats = playerData:WaitForChild("Stats")

stats.Gold.Value += 100  -- adds 100 gold
print("New gold:", stats.Gold.Value)

-- The system will automatically detect this change
-- and save it on the next autosave tick.

:paw_prints: Working With Container Folders (Pets, Characters, etc.)

You might’ve noticed folders like PetsFolder and Characters in the template example.
These are special container folders, perfect for systems where you need to store multiple entries dynamically, like:

  • Player pets
  • Weapons inventory
  • Owned characters or skins

Each container folder needs two attributes:

  • IsContainer = true
  • TemplateName = "NameOfYourTemplateFolder"

When the player joins, the DataHandler looks for these containers and uses their template to automatically rebuild entries that were saved previously.

For example:

DataTemplate
└── PetsFolder
    ├── PetTemplate
    │   ├── Shiny (BoolValue)
    │   ├── Equipped (BoolValue)
    │   ├── Level (IntValue)
    │   ├── EXP (IntValue)
    │   └── PetName (StringValue)
    └── (Attributes: IsContainer = true, TemplateName = "PetTemplate")

Then, if you want to give the player a new pet:

local playerData = game.Players["player name"].PlayerData
local petsFolder = playerData:WaitForChild("PetsFolder")
local petTemplate = game.ServerStorage.DataTemplate.PetsFolder:WaitForChild("PetTemplate")

-- Create a new pet
local newPet = petTemplate:Clone()
newPet.PetName.Value = "Fluffy"
newPet.Level.Value = 1
newPet.Parent = petsFolder
newPet.Name = RandomNumber() --Or a name of your choice, i recommend them being unique

That’s it. The DataHandler will automatically include Fluffy in the player’s save data, along with its level, XP, and attributes.
When the player rejoins, that same pet will be rebuilt from the saved data with all its properties intact.


:puzzle_piece: Extra Info

  • Default autosave interval: 60 seconds (you can change this)
  • Maximum retry attempts per save: 5
  • Works perfectly with folders and ValueBase objects
  • Fully recursive — handles deeply nested data structures
  • Automatically adds a Loaded BoolValue inside the player as (player.Loaded) when data finishes loading (which I recommend checking before grabbing player data)

:package: Get The Module

Here is the full script. Simply make a Module Script, and then paste this code into it:

--[[

	--> Kyonabc <--
	
	
	Simple Store (v2.0)
	----------------------------------------
	📦 Example Usage:
	
	local SimpleStore = require(game.ReplicatedStorage.DataHandler.SimpleStore

	-- Force save for a player
	SimpleStore:SaveData(player)

	-- Debug player data
	print(SimpleStore:GetData(player))

	----------------------------------------
	💡 Summary:
	- Clones your DataTemplate from ServerStorage under each player.
	- Handles both regular folders and *dynamic container folders*.
	- Containers (e.g. PetsFolder, Characters) can define their own item templates.
	- Automatically detects and rebuilds folders & value objects at runtime.
	- Includes autosave, retry logic, and detailed logs.

	----------------------------------------
	📂 Example DataTemplate layout (ServerStorage/DataTemplate)

	DataTemplate
	├── Stats
	│   ├── Level (IntValue)
	│   └── Gold (IntValue)
	├── PetsFolder
	│   ├── PetTemplate (Folder)
	│   │   ├── Shiny (BoolValue)
	│   │   ├── Equipped (BoolValue)
	│   │   ├── Level (IntValue)
	│   │   ├── EXP (IntValue)
	│   │   └── PetName (StringValue)
	│   └── (Attributes: IsContainer = true, TemplateName = "PetTemplate")
	├── Characters
	│   ├── CharacterTemplate (Folder)
	│   │   ├── HP (IntValue)
	│   │   ├── Stamina (IntValue)
	│   │   ├── Name (StringValue)
	│   │   └── EquippedWeapon (StringValue)
	│   └── (Attributes: IsContainer = true, TemplateName = "CharacterTemplate")
--]]

----------------------------------------
-- ⚙️ Dependencies & Setup
----------------------------------------

local DataHandler = {}
DataHandler.__index = DataHandler

local ds = game:GetService("DataStoreService")
local players = game:GetService("Players")
local ss = game:GetService("ServerStorage")
local http = game:GetService("HttpService")

local playerData = ds:GetDataStore("PlayerData")
local dataTemplate = ss:FindFirstChild("DataTemplate")

----------------------------------------
-- 🧩 Configuration
----------------------------------------
local AUTO_SAVE_INTERVAL = 60 -- seconds
local MAX_RETRIES = 5

----------------------------------------
-- 🧰 Core Helper Functions
----------------------------------------

-- 🧬 Clone DataTemplate contents into player
function DataHandler:CreatePlayerData(player)
	if not dataTemplate then
		warn("[DataHandler] ⚠️ No DataTemplate found in ServerStorage.")
		return
	end

	for _, obj in ipairs(dataTemplate:GetChildren()) do
		local clone = obj:Clone()
		clone.Parent = player
	end
end

-- 🔍 Returns whether a folder is a special container
local function isContainerFolder(folder)
	return folder:GetAttribute("IsContainer") == true
end

----------------------------------------
-- 💾 Serialization
----------------------------------------

-- Serializes any ValueBase or Folder recursively
function DataHandler:SerializePlayerData(player)
	local data = {}

	for _, folder in ipairs(player:GetChildren()) do
		if folder:IsA("Folder") then
			if isContainerFolder(folder) then
				-- 📦 Specialized container folder
				local templateName = folder:GetAttribute("TemplateName")
				local template = folder:FindFirstChild(templateName)
				if not template then
					warn("[DataHandler] Missing template for container:", folder.Name)
					continue
				end
				
				data[folder.Name] = {}
				for _, item in ipairs(folder:GetChildren()) do
					if item:IsA("Folder") and item.Name ~= template.Name then
						local itemData = {}
						for _, v in ipairs(item:GetChildren()) do
							if v:IsA("ValueBase") then
								itemData[v.Name] = v.Value
							end
						end
						data[folder.Name][item.Name] = itemData
					end
				end
			else
				-- 🧱 Regular folder
				data[folder.Name] = {}
				for _, valueObj in ipairs(folder:GetChildren()) do
					if valueObj:IsA("ValueBase") then
						data[folder.Name][valueObj.Name] = valueObj.Value
					end
				end
			end
		elseif folder:IsA("ValueBase") then
			-- 🌿 Direct values under player
			data[folder.Name] = folder.Value
		end
	end

	return data
end

----------------------------------------
-- 📥 Deserialization
----------------------------------------

function DataHandler:LoadData(player, data)
	for folderName, values in pairs(data) do
		local folder = player:FindFirstChild(folderName)
		if not folder then continue end

		if isContainerFolder(folder) then
			-- 🔁 Dynamic item reconstruction
			local templateName = folder:GetAttribute("TemplateName")
			local template = folder:FindFirstChild(templateName)
			if not template then
				warn("[DataHandler] Missing template for", folderName)
				continue
			end

			for itemName, itemValues in pairs(values) do
				local newItem = template:Clone()
				newItem.Name = itemName
				newItem.Parent = folder

				for key, val in pairs(itemValues) do
					local valueObj = newItem:FindFirstChild(key)
					if valueObj and valueObj:IsA("ValueBase") then
						valueObj.Value = val
					end
				end
			end
		elseif typeof(values) == "table" then
			-- Standard folder
			for key, val in pairs(values) do
				local valueObj = folder:FindFirstChild(key)
				if valueObj and valueObj:IsA("ValueBase") then
					valueObj.Value = val
				end
			end
		else
			-- Plain value
			local valueObj = player:FindFirstChild(folderName)
			if valueObj and valueObj:IsA("ValueBase") then
				valueObj.Value = values
			end
		end
	end
	if not player:FindFirstChild("Loaded") then
		local loaded = Instance.new("BoolValue",player)
		loaded.Name = "Loaded"
		loaded.Value = true
		
	end
end

----------------------------------------
-- 💾 Save / Load Wrappers
----------------------------------------

function DataHandler:SaveData(player)
	local success, err
	local data = self:SerializePlayerData(player)

	for i = 1, MAX_RETRIES do
		success, err = pcall(function()
			playerData:SetAsync(player.UserId, data)
		end)
		if success then break end
		task.wait(1 + (i * 0.5))
	end

	if success then
		print("[DataHandler] ✅ Saved data for", player.Name)
	else
		warn("[DataHandler] ❌ Failed to save data for", player.Name, ":", err)
	end
end

function DataHandler:LoadOrCreate(player)
	local success, data
	for i = 1, MAX_RETRIES do
		success, data = pcall(function()
			return playerData:GetAsync(player.UserId)
		end)
		if success or i == MAX_RETRIES then break end
		task.wait(1 + (i * 0.5))
	end

	self:CreatePlayerData(player)

	if success and data then
		self:LoadData(player, data)
		print("[DataHandler] ✅ Loaded data for", player.Name)
	else
		print("[DataHandler] ⚙️ New data created for", player.Name)
	end
end

function DataHandler:GetData(player)
	return self:SerializePlayerData(player)
end

----------------------------------------
-- 🚀 Initialization
----------------------------------------

function DataHandler:Init()
	if not dataTemplate then
		warn("[DataHandler] ❌ No DataTemplate found in ServerStorage.")
		return
	end

	players.PlayerAdded:Connect(function(player)
		self:LoadOrCreate(player)
	end)
	
	for _, player in players:GetPlayers() do
		self:LoadOrCreate(player)
	end

	players.PlayerRemoving:Connect(function(player)
		self:SaveData(player)
	end)
	
	game:BindToClose(function()
		for _, player in ipairs(players:GetPlayers()) do
			self:SaveData(player)
		end
	end)
	
	task.spawn(function()
		while true do
			task.wait(AUTO_SAVE_INTERVAL)
			for _, player in ipairs(players:GetPlayers()) do
				self:SaveData(player)
			end
		end
	end)

	print("[DataHandler] ✅ Initialized (Dynamic Container Support Active)")
end

return DataHandler

Here is an example:

local players = game:GetService("Players")
local replicatedStorage = game:GetService("ReplicatedStorage")
local simpleStore = require(replicatedStorage:WaitForChild("SimpleStore"))

simpleStore:Init() --initializes the datastore

local generatedIDs = {}

local function GenerateRandomID(Object: Instance)

	local Chance = math.random(1,100000)

	if generatedIDs[Chance] then

		return GenerateRandomID(Object) -- reroll if exists

	end

	table.insert(generatedIDs,Chance)
	Object:SetAttribute("ID",Chance)

	return Chance
end

game.Players.PlayerAdded:Connect(function(plr)
	local characterFolder = plr:WaitForChild("Characters")
	local template = characterFolder.CharacterTemplate --or whatever you name it to be inside dataTemplate
	
	local newCharacter = template:Clone()
	
	GenerateRandomID(newCharacter)
	local ID = newCharacter:GetAttribute("ID")
	
	--gives them a random ID (Not neccessary, but good for inventory style systems)
	newCharacter.Name = ID
	newCharacter.Parent = characterFolder
	
	newCharacter.Shiny.Value = true --value inside to save
	
	
end)

You can edit and tweak the values inside of this script all you want, and you don’t have to give me credit if you don’t want to. if

you have any questions, or any new ideas, please ask me. I’m new to creating resources on the dev forum. I used AI-human pairing to create this succesfully, and used the framework from my own games datastore to make it. If you would like to check out my game, it’s linked below. Thank you for viewing my resource, I hope you enjoy Simple Store.

My game Link: ⚔️Medieval Simulator⚔️ (Update 1.3) - Roblox

2 Likes

I tried to explain in a fairly well way, but i think it was a bit of over explaining when all you really do is take the script, initilize it, and then all the values inside the player will save and you can edit them mid game. However, I decided to take the character folders because it’s useful for inventory style systems and it’s fairly easy once you get the hang of it. It’s easy to detect changes and values on the client and server all by just looking inside the player, but what do you guys think?

why do you wait 1 + (i * 0.5) between retries? it just seems so random to increase the delay each time it fails.

Also do you have a method in place to prevent it from possibly overwriting current data with old data if the retries last until the next save attempt?

finally, there’s probably a better way to deal with auto saves than a loop, for example

local SaveData
SaveData = function()
	for _, v in players do
		--save data
	end
	task.delay(AUTO_SAVE_INTERVAL, SaveData)
end

and to start it, you’d simply just call SaveData() once

the reason this is better, is because it doesn’t rely on a loop, it’s using the task scheduler, and recursive functions

that being said, having a way to STOP automatic saving would be a good plan

That is a technique used to alleviate load on the server handling the request, in the event it goes down. If you think about it, if a service goes down, users of that service will start sending more requests, as retry mechanisms, but that then increases the load, which isn’t desirable. Exponential backoff is more friendly to those services

In my datastore script, I use an actual exponential function. It has a very steep curve, but since retries also take longer, the amount of retries stays low as well (so it would take a very very long time to reach ridiculously long wait times)
image

2 Likes

In the code, do you check that the data was loaded before attempting to save it? I’ve often seen people make this mistake, where user data is completely wiped, because their datastore system (sometimes) saves the data before it loaded (which is usually the empty template)

Other than that, I will refer you to this reply and this reply, where I ramble about mechanisms used to avoid issues of data being overwritten, or duplication exploits being possible. Your datastore system doesn’t have any of those mechanisms, so I would encourage you to read about it :P

The resource seems nice, and it would be quite good if had those safety mechanisms, but I don’t think, in its current state, that it should be used for any game where a reliable datastore system is needed. But keep up the good work, and keep learning!

1 Like

Interesting, ill be sure to update it. Appreciate the advice :folded_hands:

1 Like