TutorialsintermediateUnderstanding DataStores
Intermediate

Understanding DataStores

Save and load player data that persists between sessions. Master DataStoreService, handle errors properly, and build a complete coins save system from scratch.

understanding-datastores.lua
-- PLACE IN: ServerScript (inside ServerScriptService)
-- REQUIRES: A "leaderstats" folder will be created automatically by this script
--           No RemoteEvents needed - leaderstats are handled by Roblox automatically

-- ============================================================
--  COINS SAVE SYSTEM - Fully Commented for Complete Beginners
--  Every single line is explained so you know exactly what
--  is happening and WHY it's written that way.
-- ============================================================

-- ══════════════════════════════════════════════
--  SECTION 1: GET THE SERVICES WE NEED
-- ══════════════════════════════════════════════

-- "Services" are Roblox's built-in tools. We ask Roblox to give
-- us access to them using GetService().

-- DataStoreService lets us SAVE and LOAD data between sessions.
-- Without this, all coins would disappear when the player leaves!
local DataStoreService = game:GetService("DataStoreService")

-- Players service lets us detect when players join or leave the game.
local Players = game:GetService("Players")

-- ══════════════════════════════════════════════
--  SECTION 2: SET UP THE DATA STORE
-- ══════════════════════════════════════════════

-- GetDataStore() creates (or opens) a "database" with this name.
-- Think of it like a filing cabinet. "CoinsData" is the label on the cabinet.
-- All players' coin data will be stored inside this one cabinet.
-- You can rename "CoinsData" to anything you like.
local CoinsDataStore = DataStoreService:GetDataStore("CoinsData")

-- ══════════════════════════════════════════════
--  SECTION 3: CONFIGURATION (Easy to change!)
-- ══════════════════════════════════════════════

-- These are the settings you're most likely to want to change.
-- Putting them at the top makes them easy to find.

-- How many coins a brand new player starts with.
local STARTING_COINS = 0

-- How often (in seconds) the system auto-saves every player's data.
-- 60 seconds = save every 1 minute while they play.
local AUTO_SAVE_INTERVAL = 60

-- ══════════════════════════════════════════════
--  SECTION 4: THE SAVE FUNCTION
-- ══════════════════════════════════════════════

-- This is a "function" - a block of reusable code we can call by name.
-- We give it a player, and it saves that player's coins to the DataStore.
-- We'll call this function when a player leaves AND on a timer.
local function saveData(player)

	-- First, we find the "leaderstats" folder inside the player.
	-- leaderstats is a special Roblox folder that shows stats on the leaderboard.
	-- FindFirstChild() looks for something by name - it returns nil if not found.
	local leaderstats = player:FindFirstChild("leaderstats")

	-- If leaderstats doesn't exist yet (e.g., data still loading), stop here.
	-- The "if not" means "if this thing is missing / doesn't exist".
	if not leaderstats then
		-- "return" exits the function immediately. Nothing below this runs.
		return
	end

	-- Now we find the "Coins" value inside the leaderstats folder.
	local coinsValue = leaderstats:FindFirstChild("Coins")

	-- Same check - if Coins doesn't exist for some reason, stop here safely.
	if not coinsValue then
		return
	end

	-- Each player has a unique "UserId" number that NEVER changes,
	-- even if they change their username. We use this as the "key"
	-- (the label on the folder) to store their data in the DataStore.
	local playerKey = "Player_" .. player.UserId
	-- Example result: "Player_1234567890"
	-- The ".." operator joins two strings together.

	-- pcall stands for "protected call". It runs code that might fail
	-- (like saving data) and CATCHES any errors instead of crashing the game.
	-- It returns two things:
	--   success = true if it worked, false if it failed
	--   errorMessage = the error text if it failed (nil if success)
	local success, errorMessage = pcall(function()

		-- SetAsync() saves data to the DataStore.
		-- "playerKey" is WHERE to save it (which player's folder).
		-- "coinsValue.Value" is WHAT to save (the actual number of coins).
		CoinsDataStore:SetAsync(playerKey, coinsValue.Value)

	end)

	-- Now we check if the save worked or failed.
	if success then
		-- The save worked! Print a message to the Output window (only visible in Studio).
		print("✅ Saved " .. coinsValue.Value .. " coins for " .. player.Name)
	else
		-- The save failed. Print a warning so you know something went wrong.
		-- "warn()" prints in orange/yellow in the Output - easier to spot than print().
		warn("❌ Failed to save data for " .. player.Name .. " | Error: " .. errorMessage)
	end

end

-- ══════════════════════════════════════════════
--  SECTION 5: THE LOAD FUNCTION (runs when player joins)
-- ══════════════════════════════════════════════

-- This function runs when a player joins the game.
-- It creates their stats on the leaderboard AND loads their saved coins.
local function onPlayerAdded(player)

	-- ── STEP 1: Create the leaderboard stats ──

	-- Instance.new() creates a new Roblox object.
	-- "Folder" is the type of object - we're making a folder.
	local leaderstats = Instance.new("Folder")

	-- Give the folder the special name "leaderstats".
	-- Roblox automatically shows any values inside a folder named exactly
	-- "leaderstats" on the in-game leaderboard. The name must be exact!
	leaderstats.Name = "leaderstats"

	-- IMPORTANT RULE: Always set Parent LAST.
	-- Setting Parent causes Roblox to "register" the object in the game world.
	-- If we parent it first and then set properties, it can cause tiny slowdowns.
	-- Here we parent the folder to the player so they own it.
	leaderstats.Parent = player

	-- Now create an "IntValue" - this is a Roblox object that holds a whole number.
	-- It will show up on the leaderboard because it's inside leaderstats.
	local coinsValue = Instance.new("IntValue")

	-- Name it "Coins" - this is the label that appears on the leaderboard.
	coinsValue.Name = "Coins"

	-- Set the starting value to our config variable from Section 3.
	-- This will be overwritten below if we find saved data.
	coinsValue.Value = STARTING_COINS

	-- Parent it to leaderstats so it appears on the leaderboard.
	coinsValue.Parent = leaderstats

	-- ── STEP 2: Load saved data from the DataStore ──

	-- Build the same key we use in the save function.
	-- It must match exactly, or we'll look in the wrong "folder"!
	local playerKey = "Player_" .. player.UserId

	-- Use pcall again to safely attempt loading data.
	-- If the DataStore is down or there's a network error, this won't crash the game.
	local success, savedCoins = pcall(function()

		-- GetAsync() retrieves data from the DataStore using the key.
		-- It returns whatever was saved there, or "nil" if nothing was saved yet
		-- (e.g., a brand new player who has never played before).
		return CoinsDataStore:GetAsync(playerKey)

	end)

	if success then

		-- The load worked! Now check if we actually got data back.
		-- "savedCoins" will be nil for brand new players (never saved before).
		-- The "~=" operator means "not equal to".
		if savedCoins ~= nil then

			-- We have saved data! Overwrite the default starting value
			-- with the player's actual saved coin count.
			coinsValue.Value = savedCoins

			print("📂 Loaded " .. savedCoins .. " coins for " .. player.Name)

		else

			-- No saved data found = this is a brand new player!
			-- They'll keep the STARTING_COINS value we set earlier.
			print("🆕 New player! Gave " .. STARTING_COINS .. " starting coins to " .. player.Name)

		end

	else

		-- The load failed. savedCoins actually holds the error message here
		-- because when pcall fails, the second return value is the error.
		warn("❌ Failed to load data for " .. player.Name .. " | Error: " .. savedCoins)

		-- We still let the player in with default coins - never block a player
		-- from playing just because the DataStore had a hiccup!

	end

end

-- ══════════════════════════════════════════════
--  SECTION 6: HANDLE PLAYERS LEAVING (SAVE ON EXIT)
-- ══════════════════════════════════════════════

-- This function runs automatically when a player leaves the game.
-- It's critical we save here so their progress is never lost!
local function onPlayerRemoving(player)

	print("👋 " .. player.Name .. " is leaving - saving their data...")

	-- Call the save function we wrote in Section 4.
	-- We pass the player as an "argument" (an input to the function).
	saveData(player)

end

-- ══════════════════════════════════════════════
--  SECTION 7: HANDLE SERVER SHUTDOWN (Emergency Save)
-- ══════════════════════════════════════════════

-- "game:BindToClose()" runs a function when the SERVER is shutting down.
-- This is important because if the server closes suddenly (e.g., Roblox updates),
-- PlayerRemoving might not fire for every player in time.
-- This is our safety net!
game:BindToClose(function()

	print("🔴 Server shutting down - saving all players...")

	-- Get a list of ALL players currently in the game.
	-- Players:GetPlayers() returns a table (list) of player objects.
	local allPlayers = Players:GetPlayers()

	-- "for" loop goes through every item in the list one by one.
	-- "_" is used when we don't need the index number (just the player object).
	for _, player in allPlayers do

		-- Save each player's data.
		saveData(player)

	end

	print("✅ All players saved! Server closing safely.")

end)

-- ══════════════════════════════════════════════
--  SECTION 8: AUTO-SAVE LOOP (Saves while playing)
-- ══════════════════════════════════════════════

-- Even if the server never shuts down cleanly, we auto-save every player
-- periodically. This means players lose at most AUTO_SAVE_INTERVAL seconds
-- of progress if something goes horribly wrong.

-- task.spawn() runs a block of code in the "background".
-- This means the auto-save loop runs on its own without
-- freezing or blocking the rest of the script.
task.spawn(function()

	-- "while true do" creates a loop that runs forever.
	-- The script will keep repeating this block of code endlessly.
	while true do

		-- task.wait() pauses the loop for the number of seconds we set.
		-- This is like setting a timer. The loop "sleeps" and then wakes up.
		-- IMPORTANT: We use task.wait(), NOT wait() - the old wait() is deprecated!
		task.wait(AUTO_SAVE_INTERVAL)

		-- After waiting, save every player currently in the server.
		local allPlayers = Players:GetPlayers()

		-- Only print the auto-save message if there's at least 1 player.
		-- "#allPlayers" gives us the number of items in the list.
		if #allPlayers > 0 then
			print("💾 Auto-saving " .. #allPlayers .. " player(s)...")
		end

		for _, player in allPlayers do
			saveData(player)
		end

	end

end)

-- ══════════════════════════════════════════════
--  SECTION 9: CONNECT EVENTS (Make everything actually run!)
-- ══════════════════════════════════════════════

-- Up until now we've only DEFINED functions. Nothing has actually run yet!
-- We now "connect" our functions to Roblox events so they fire at the right time.

-- PlayerAdded fires whenever a new player joins the game.
-- We tell it to call our onPlayerAdded function, passing the player automatically.
Players.PlayerAdded:Connect(onPlayerAdded)

-- PlayerRemoving fires whenever a player leaves the game.
-- We tell it to call our onPlayerRemoving function.
Players.PlayerRemoving:Connect(onPlayerRemoving)

-- ── Handle players already in the game ──
-- If this script runs AFTER some players have already joined
-- (can happen in Studio when testing), PlayerAdded won't fire for them.
-- This loop manually calls onPlayerAdded for anyone already in the server.
for _, player in Players:GetPlayers() do
	-- task.spawn() runs onPlayerAdded for each existing player
	-- without blocking - each one loads independently.
	task.spawn(onPlayerAdded, player)
end

print("🚀 Coin Save System loaded successfully!")

-- ══════════════════════════════════════════════
--  BONUS: HOW TO GIVE/TAKE COINS FROM OTHER SCRIPTS
-- ══════════════════════════════════════════════
--
-- From any other ServerScript, you can change a player's coins like this:
--
--   local player = -- (however you get the player)
--   local coins = player.leaderstats.Coins
--
--   -- Give 50 coins:
--   coins.Value = coins.Value + 50
--
--   -- Take 30 coins (with a check so it doesn't go below 0):
--   coins.Value = math.max(0, coins.Value - 30)
--
--   -- Set coins to exactly 100:
--   coins.Value = 100
--
-- The save system handles saving automatically - you just change the Value!
-- ══════════════════════════════════════════════

📍 **Setup:**

- **Place this script** inside `ServerScriptService` as a `Script` (not a LocalScript)
- **Enable Studio API access** → Go to `Game Settings → Security → Enable Studio Access to API Services` (required for DataStores to work in Studio testing)
- **Test it** → Hit Play, check the Output window for the `✅` and `📂` messages confirming it's working, and watch the leaderboard appear automatically
- **Change `STARTING_COINS`** at the top of Section 3 if you want new players to begin with coins (e.g., `100` for a starter bonus)
- **Give coins from other scripts** using `player.leaderstats.Coins.Value += 50` from any ServerScript — the auto-save handles the rest every 60 seconds
Luanaut

Want this written for your specific game?

Describe exactly what you need and get working Luau code in seconds.

Try the AI Generator →