Skip to content

Data stores

Nova implements the classic DataStoreService surface and backs it with the persistence service on the official platform. The core operations work:

  • DataStoreService:GetDataStore(name, scope)
  • DataStoreService:GetGlobalDataStore()
  • DataStoreService:GetOrderedDataStore(name, scope)
  • GlobalDataStore:GetAsync(key)
  • GlobalDataStore:SetAsync(key, value)
  • GlobalDataStore:IncrementAsync(key, delta)
  • GlobalDataStore:UpdateAsync(key, transformFunction)
  • GlobalDataStore:OnUpdate(key, callback)
  • OrderedDataStore:GetSortedAsync(ascending, pageSize, minValue, maxValue)

Server only

Data stores are server-side. Call them from a Script or a ModuleScript used by a server script, never from a LocalScript.

Basic usage

local DataStoreService = game:GetService("DataStoreService")
local playerStore = DataStoreService:GetDataStore("PlayerData")

local function keyFor(player: Player): string
    return "player_" .. player.UserId
end

local function load(player: Player)
    local ok, result = pcall(function()
        return playerStore:GetAsync(keyFor(player))
    end)

    if not ok then
        warn("load failed for", player.Name, result)
        return nil
    end

    return result
end

local function save(player: Player, data)
    local ok, err = pcall(function()
        playerStore:SetAsync(keyFor(player), data)
    end)

    if not ok then
        warn("save failed for", player.Name, err)
    end
end

game:GetService("Players").PlayerAdded:Connect(function(player)
    local data = load(player) or { Coins = 0 }
    print(player.Name, "has", data.Coins, "coins")
end)

Wrap every data-store call in pcall. A failed request should not take down the player's session.

UpdateAsync and session locking

UpdateAsync is the safe way to read-modify-write a key. The callback receives the current value and returns the new value (or nil to cancel the write).

local function addCoins(player: Player, amount: number)
    local ok, newValue = pcall(function()
        return playerStore:UpdateAsync(keyFor(player), function(current)
            current = current or { Coins = 0 }
            current.Coins += amount
            return current
        end)
    end)

    if not ok then
        warn("update failed", newValue)
    end
end

For a full session-lock pattern:

local SESSION_TTL = 60 -- seconds; refresh while the player is in the server

local function acquire(player: Player)
    local key = keyFor(player)
    local ok, acquired = pcall(function()
        return playerStore:UpdateAsync(key, function(current)
            current = current or { Coins = 0 }

            local now = os.time()
            if current.Lock and current.Lock.expires > now
                and current.Lock.jobId ~= game.JobId then
                return nil -- someone else owns the session
            end

            current.Lock = {
                jobId = game.JobId,
                expires = now + SESSION_TTL,
            }
            return current
        end)
    end)

    return ok and acquired ~= nil
end

Refresh the lock periodically with another UpdateAsync, and release it on PlayerRemoving by setting Lock = nil. If the server crashes, the TTL lets the next server take over.

OrderedDataStore

OrderedDataStore is for leaderboards. Values must be integers.

local leaderboard = DataStoreService:GetOrderedDataStore("Coins")

local function topTen()
    local ok, pages = pcall(function()
        return leaderboard:GetSortedAsync(false, 10) -- descending
    end)

    if not ok then
        warn("leaderboard failed", pages)
        return {}
    end

    return pages:GetCurrentPage()
end

GetSortedAsync returns a pages object. Use AdvanceToNextPageAsync() and IsFinished to walk it.

Differences from modern Roblox

  • DataStoreOptions, DataStoreGetOptions, DataStoreSetOptions, and the request-budget API are not implemented yet. GetDataStore takes only (name, scope).
  • RemoveAsync is not part of Nova's surface.
  • Request budgets and throttling are enforced by the platform backend rather than the Roblox data-store service. Treat limits as real and back off on errors.
  • The backend stores data per universe. A place must belong to a universe for persistence to resolve.

Debugging

Error Meaning
DataStore ... does not exist / empty result The key has never been written, or the universe/scope is wrong.
403/401 from persistence The server is not authenticated to the platform backend.
UpdateAsync returns nil Your callback returned nil (often a lock conflict).
Data appears in Studio but not live Studio and the live server may resolve different universes. Check the place/universe mapping.

Next