Skip to main content

Replication and remote ops

Clients see the data the server chooses to share, and never write it. When a player wants something changed, the client asks for a declared op and the server decides.

Visibility​

Each path is private (the default), owner or public. Owner paths reach their own player, public paths reach everyone, private paths never leave the server. A path under a shared one can't be stricter, since the shared path sends everything under it, and a map that tries is refused when the store is made.

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Geode = require(ReplicatedStorage.Packages.Geode)

local PlayerData = Geode.players({
name = "PlayerData",
template = { coins = 0, level = 1, inventory = {}, banReason = "" },
replicate = { coins = "owner", inventory = "owner", level = "public" },
})

After each commit, only the visible paths that changed are queued, and the queue goes out once per frame, so a burst of writes costs one remote call. A client that joins or asks to resync gets a full snapshot first.

Remote ops​

Geode.remoteOp lets clients ask for an op on their own profile. Each request is checked in order of cost: the attempt rate limit first, then each argument against its schema, then the store's pipeline with a player source, so every guard knows who asked.

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Geode = require(ReplicatedStorage.Packages.Geode)

local PRICES = { sword = 100, shield = 75 }

Geode.op("guideBuyItem", function(tx, item: string)
tx:decrement("coins", PRICES[item])
tx:append("inventory", item)
end)

local PlayerData = Geode.players({
name = "PlayerData",
template = { coins = 0, inventory = {} },
replicate = { coins = "owner", inventory = "owner" },
ops = { "guideBuyItem" },
guards = { Geode.guard.range("coins", { min = 0 }) },
})

Geode.remoteOp("guideBuyItem", {
store = PlayerData.store,
args = { Geode.t.enum({ "sword", "shield" }) },
rateLimit = { count = 5, window = 10, action = "drop" },
})

Remote ops send no answer. The client sees the result through replication, which also keeps a refused request from revealing why it was refused.

The client​

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Geode = require(ReplicatedStorage.Packages.Geode)

local client = Geode.Client.start()

local coins = client:atom("coins")
local inventory = client:atom("inventory")

local worth = client:computed(function()
return (coins:get() or 0) + #(inventory:get() or {}) * 50
end)

worth:subscribe(function(value)
print("total worth", value)
end)

client:op("guideBuyItem", "sword")

atom(path, userId) reads another player's public paths. computed tracks the atoms it reads and runs again when any of them changes. To use Charm instead, pass the Charm module to client:charm(Charm) and its atom(path) returns Charm atoms that follow the replicated data.

A roblox-ts package is coming soon. It will expose the same client with the same names, typed from the typings in types/.