Ledger

Player data for Roblox that can't be duplicated or lost halfway. You describe each change, your own function decides if it's allowed, and Ledger saves it exactly once.

type PlayerData = { Gold: number, Items: { string } }

type PlayerOps = {
	AddGold: { Amount: number },
	SpendGold: { Amount: number },
	BuyItem: { Item: string, Price: number },
}

local PlayerStore = Ledger.New<<PlayerData, PlayerOps>>({
	Name = "PlayerData",
	Keys = "Player",
	Default = { Gold = 0, Items = {} },
	MustExist = false,
	Erasable = true,
	Balances = {
		Gold = { Credit = "AddGold", Debit = "SpendGold" },
	},
	Reducer = function(Data, Op)
		if Op.Kind == "BuyItem" then
			if Data.Gold < Op.Price or table.find(Data.Items, Op.Item) then
				return nil
			end

			local Items = table.clone(Data.Items)
			table.insert(Items, Op.Item)

			local New = table.clone(Data)
			New.Gold -= Op.Price
			New.Items = Items
			return New
		end

		return nil
	end,
})

local Session = PlayerStore:Load(Player)
if Session then
	local Ok, Why = Session:Apply({ Kind = "BuyItem", Item = "Sword", Price = 50 })
	if not Ok then
		warn("Couldn't buy the sword:", Why)
	end
end

Every change saved once

A change is never applied twice, even when a server crashes mid-save or a DataStore outage makes Ledger send it again.

Your rules decide

Your reducer says whether each change is allowed. If a player can't afford something, it's refused on every server, every time.

Trades that can't half happen

Ledger.Tx changes several players or keys at once: all of them or none. It works across servers, and for players who are offline.

Answers you can act on

Every call answers true, or one of 15 reasons. Each reason means one thing and has one thing to do about it.

Limited items and counters

Sell a fixed stock from every server at once without overselling, and keep totals the whole game adds to.

Updates that keep old saves

Change the shape of your data with migrations. Old servers and new ones can run side by side during an update.