Open Source ยท MIT License

State Sync
Made Simple

Server-authoritative data channels with automatic client replication, batched networking, and event-driven updates for Roblox.

โšก

Batched Networking

All mutations queued and sent as a single payload per frame.

๐Ÿ”’

Server-Authoritative

Data lives on the server. Clients get read-only caches with rate limiting.

๐Ÿ“ก

Event-Driven

Subscribe to changes at any path โ€” nested paths, arrays, dictionaries.

๐Ÿงน

Zero Memory Leaks

Automatic cleanup on destroy. PlayerRemoving cleanup built in.

๐Ÿ”—

Fluent API

Chain mutations: channel:Set():Set():Increase()

๐Ÿ›ก๏ธ

Strict Typing

Full Luau type annotations for autocomplete and analysis.

Getting Started

Installation (Roblox Studio)

  1. Create a ModuleScript named Synchronizer inside ReplicatedStorage/Packages
  2. Create a child ModuleScript named Channel inside it
  3. Copy the contents of src/Synchronizer.luau and src/Channel.luau
ReplicatedStorage
โ””โ”€โ”€ Packages
    โ”œโ”€โ”€ Synchronizer         โ† Synchronizer.luau
    โ”‚   โ””โ”€โ”€ Channel          โ† Channel.luau
    โ””โ”€โ”€ Signal               โ† (dependency)

Via Rojo / Git

git clone https://github.com/zSkanz/Synchronizer.git

Dependencies

Synchronizer requires a Signal module as a sibling inside Packages. We recommend sleitnick/signal.

Basic Usage

Server

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

Players.PlayerAdded:Connect(function(player)
    local channel = Synchronizer:Create(player.UserId, {
        coins = 0;
        level = 1;
        inventory = {};
    })

    channel:AddListener(player)
    channel:Set("coins", 100)
    channel:Increase("coins", 50)
    channel:InsertOnArray("inventory", "Sword")
end)

Players.PlayerRemoving:Connect(function(player)
    Synchronizer:Destroy(player.UserId)
end)

Client

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

local channel = Synchronizer:Wait(game.Players.LocalPlayer.UserId)

channel:OnChanged("coins", function(newValue, oldValue)
    print("Coins:", oldValue, "โ†’", newValue)
end, true)

channel:OnArrayInserted("inventory", function(item, index)
    print("New item:", item, "at index", index)
end)

local coins = channel:Get("coins")
local allData = channel:GetTable()

How It Works

Server Client โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Synchronizer โ”‚ โ”‚ Synchronizer โ”‚ โ”‚ โ”œโ”€โ”€ Channel A โ”‚ FireClient โ”‚ โ”œโ”€โ”€ Channel A โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Set() โ”€โ”€โ”€โ”€โ”ผโ”€โ”€(batched/frame)โ”€โ”ผโ”€โ”€โ”‚ โ”œโ”€โ”€ CacheTable โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Queue โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บโ”‚ โ”‚ โ”œโ”€โ”€ OnChanged โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ Listeners โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ Signals โ”‚ โ”‚ โ””โ”€โ”€ Channel B โ”‚ โ”‚ โ””โ”€โ”€ Channel B โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
  1. Server mutates data via Set, Increase, InsertOnArray, etc.
  2. Mutations are queued and deduplicated within the frame.
  3. On RunService.Stepped, all queued actions are batched and sent to listeners.
  4. Client receives the batch, updates its cache, and fires local signals.

Advanced Usage

Path System

Dot-separated paths navigate nested data:

channel:Set("stats.health", 80)
channel:Increase("stats.mana", 10)
channel:OnChanged("stats.health", function(newHP, oldHP)
    print("HP:", oldHP, "โ†’", newHP)
end)

Chaining Mutations

All mutation methods return self. Everything is batched into a single network payload:

channel
    :Set("coins", 500)
    :Set("level", 10)
    :Increase("coins", 100)

ClearSignal โ€” Dynamic Cleanup

Clean up all signals matching a substring:

channel:ClearSignal("hero_abc")

WaitAndCall โ€” Non-Yielding

Synchronizer:WaitAndCall(player.UserId, function(channel)
    channel:OnChanged("coins", function(newValue)
        updateCoinUI(newValue)
    end, true)
end)

Configuration

local Settings = {
    MAX_REQUESTS_PER_SECOND = 10;  -- Rate limit for client requests
    DESTROY_CLEANUP_DELAY = 5;     -- Seconds before remote cleanup
}

Security

  • Listener Validation โ€” Clients can only request data from channels they listen to.
  • Rate Limiting โ€” Clients exceeding MAX_REQUESTS_PER_SECOND are blocked.
  • Data Validation โ€” Incoming client data is type-checked.
  • Cache Isolation โ€” Each client has its own independent cache table.

Synchronizer API

Synchronizer:Create(index, referenceTable?)Server
Creates a new channel. If one already exists at that index, returns the existing one.
Param index any โ€” Unique identifier
Param referenceTable {[any]: any}? โ€” Initial data
Returns Channel
local channel = Synchronizer:Create(player.UserId, {
    coins = 0;
    level = 1;
})
Synchronizer:Destroy(index)Server
Destroys a channel and cleans up all resources.
Param index any
Synchronizer:Get(index)Shared
Returns the channel at the given index, or nil.
Param index any
Returns Channel?
Synchronizer:GetAllChannels()Shared
Returns all active channels.
Returns {[any]: Channel}
Synchronizer:GetTableFromChannel(index)Shared
Returns the raw data table from a channel, or nil.
Param index any
Returns {[any]: any}?
Synchronizer:Wait(index)SharedYields
Yields until a channel with the given index exists.
Param index any
Returns Channel
Synchronizer:WaitAndCall(index, callback)Shared
Calls the callback when the channel exists. Does not yield.
Param index any
Param callback (Channel) โ†’ ()
Synchronizer Events
PropertyTypeDescription
OnChannelCreatedSignal<Channel>Fires when a new channel is created
OnChannelDestroyedSignal<Channel>Fires when a channel is destroyed
OnChannelListenerAddedSignal<Channel, Player>Player added as listener
OnChannelListenerRemovedSignal<Channel, Player>Player removed as listener

Channel API

Channels are created via Synchronizer:Create(). Mutation methods are server-only and return self for chaining.

Mutations

Channel:Set(path, value)ServerChainable
Sets the value at the given path.
Param path string
Param value any
Returns Channel
Channel:Increase(path, value)ServerChainable
Increments a numeric value at the given path.
Param path string
Param value number
Returns Channel
Channel:InsertOnArray(path, value, targetIndex?)Server
Inserts a value into an array. Optionally specify position.
Param path string
Param value any
Param targetIndex number?
Returns number?
Channel:RemoveFromArray(path, index)Server
Removes a value from an array by index.
Param path string
Param index number
Returns any?
Channel:InsertOnDictionary(path, index, value)Server
Inserts a key-value pair into a dictionary.
Param path string
Param index any
Param value any
Channel:RemoveFromDictionary(path, index)Server
Removes a key from a dictionary.
Param path string
Param index any
Returns any?

Events

Channel:OnChanged(path, callback, forceCall?)Shared
Subscribes to value changes. forceCall = true fires immediately with current value.
Param path string
Param callback (newValue, oldValue) โ†’ ()
Param forceCall boolean?
Returns Connection
channel:OnChanged("coins", function(newVal, oldVal)
    print("Coins:", oldVal, "โ†’", newVal)
end, true)
Channel:OnArrayInserted(path, callback)Shared
Subscribes to array insertion events.
Param path string
Param callback (value, index) โ†’ ()
Returns Connection
Channel:OnArrayRemoved(path, callback)Shared
Subscribes to array removal events.
Param path string
Param callback (value, index) โ†’ ()
Returns Connection
Channel:OnDictionaryInserted(path, callback)Shared
Subscribes to dictionary insertion events.
Param path string
Param callback (value, key) โ†’ ()
Returns Connection
Channel:OnDictionaryRemoved(path, callback)Shared
Subscribes to dictionary removal events.
Param path string
Param callback (value, key) โ†’ ()
Returns Connection

Listeners

Channel:AddListener(player)ServerChainable
Adds a player as a listener for this channel.
Param listener Player
Returns Channel
Channel:RemoveListener(player)ServerChainable
Removes a player from the listener list.
Param listener Player
Returns Channel
Channel:GetListeners()Shared
Returns the list of players listening to this channel.
Returns {Player}

Getters

Channel:Get(path?)Shared
Returns value at path, or full data table if no path given.
Param path string?
Returns any
Channel:GetIndex()Shared
Returns the channel's unique index.
Returns any
Channel:GetTable()Shared
Returns the raw data table (server: reference, client: cache).
Returns {[any]: any}

Cleanup

Channel:ClearSignal(signalIdentifier)SharedChainable
Destroys all signals whose path contains the given string.
Param signalIdentifier string
Returns Channel
Channel:Destroy()Shared
Destroys this channel and all associated resources.
Channel Events
PropertyTypeDescription
OnDestroyedSignal<Channel>Fires when this channel is destroyed