zense/constella
A modular Luau toolkit for Roblox: app lifecycle, signals, cleanup, reactive state, secure networking, components, pools, zones, promises, player data and replication.
Constella
A modular, strictly typed Luau toolkit for Roblox.
Use one utility on its own, or build your whole game on it.
Made by Zense
📖 Documentation → docs/
🧪 Examples → one script per module
🗺️ Roadmap
# wally.toml
[dependencies]
Constella = "layfate/[email protected]"
Also available as a ready-to-insert model file (download Constella.rbxm) or with pesde (pesde add zense/constella). See Installation.
- Use only what you need. Every module loads on first use.
require(Constella.Signal)loads Signal and nothing else, and no system starts unless you start it. - Safe networking by default. Every client request passes rate limiting, payload caps, schema validation and your authorization check before your code runs. Clients only ever see generic error codes; the details stay on the server.
- An explicit architecture. Apps start services in dependency order, catch duplicates, missing dependencies and cycles before any code runs, keep client and server code apart, and roll back cleanly if startup fails. No globals, no singletons, no auto-discovery.
- Player data that fails closed. Migrations, template reconciliation and session-loss handling sit on top of ProfileStore's session locking. A failed load never overwrites saved data.
- Tested and measured. Unit tests, Data failure simulations, tests against real ProfileStore, Play-mode integration tests with a hostile client against real remotes, and published benchmarks against other libraries and hand-written baselines, including where Constella is slower.
Note
Constella.Data runs on ProfileStore, which you install separately. Every CI run tests it on real DataStores on real Roblox servers, including a player moving to a second server. See Data for exactly what's verified.
Modules
| Module | What it does | |
|---|---|---|
| Architecture | Core / App | Services and controllers with dependency-ordered Init/Start/Stop and injection |
| Utilities | Signal | Custom events with deterministic dispatch and error isolation |
| Cleanup | Owns connections, instances, threads and promises, and disposes them together | |
| State | Reactive values, computed state, batching and UI bindings | |
| Promise | Async chaining, cancellation, timeouts and retries | |
| Random | Weighted rolls, loot tables and luck, with the math documented | |
| Debug | Structured logging and metrics, with almost no cost when switched off | |
| Game systems | Network | Server-owned remotes with validation, rate limits and payload caps |
| Replication | Server→client State, only when the server explicitly shares it | |
| Component | Behaviour attached to CollectionService tags | |
| Pool | Bounded object reuse for projectiles and effects | |
| Zone | Enter/exit detection for players and parts | |
| Data | Player profiles on ProfileStore (server only) |
Compared with other libraries
Each module has a well-known alternative. Constella's case is that all 13 share one set of rules (strict types, explicit lifetimes, loud errors, no globals) and are tested together on real Roblox servers. If you only need one piece, the specialist library is often the right pick.
| Job | Common choice | Constella | Main difference |
|---|---|---|---|
| Secure remotes | Plain RemoteEvents; Zap, ByteNet or Blink | Network | Security first: every client request is rate limited, size capped, schema checked and authorized before your handler runs, and clients only see generic error codes. Zap, ByteNet and Blink focus on compact serialization and bandwidth instead. The two approaches can be combined |
| Player data | ProfileStore directly | Data | Built on ProfileStore: adds migrations, template reconciliation, Changed signals, kick-on-session-loss and a shutdown budget that waits for every save |
| App structure | Knit | Core | Explicit apps instead of global singletons: dependency order, cycle and environment checks before any code runs, and rollback when startup fails |
| Cleanup | Trove, Janitor, Maid | Cleanup | Level with Janitor and about 6× faster than Trove for functions on a Roblox server, with half of Trove's memory (benchmarks) |
| Signals | GoodSignal, sleitnick/signal | Signal | Catches each listener's errors and calls listeners in order. With the same error catching, Fire is within 1–11% of sleitnick/signal (depending on the server) and faster than LemonSignal; Connect+Disconnect is slower; and it uses the least memory per connection of the three |
| Promises | evaera/promise | Promise | A smaller, strictly typed core: cancellation, timeouts, all/race/allSettled, retry with backoff. About 6× faster than evaera/promise for a resolve+Then+Await and a tenth of the memory per promise; evaera/promise has a larger API and a long track record |
| Reactive state | Fusion | State | Small reactive values with property bindings, not a UI framework. For building whole interfaces declaratively, Fusion does far more |
| Zones | ZonePlus | Zone | Simple point tests against boxes and spheres, with hysteresis at the edges, at about 0.3 µs per tracked item per step. ZonePlus supports more shapes and features |
| Tagged components | sleitnick/component | Component | Each component gets its own Cleanup and an ancestor filter |
| Object reuse | PartCache | Pool | Pools any object, not only Parts, with limits on idle and active counts |
Quick start
Utilities work on their own, with no setup:
-- Any Script or LocalScript
local Constella = require(game:GetService("ReplicatedStorage").Packages.Constella)
local cleanup = Constella.Cleanup.new()
local health = Constella.State.new(100)
cleanup:Add(health:Subscribe(function(new, old)
print("Health", old, "->", new)
end))
health:Set(80)
local rarity = Constella.Random.choose({
{ Value = "Common", Weight = 75 },
{ Value = "Rare", Weight = 20 },
{ Value = "Legendary", Weight = 5 },
}, Random.new(1234)) -- seeded: the same roll every time
cleanup:Destroy() -- disconnects everything it owns
For a whole game, write services and start them with an app. This service keeps each player's coins on the server, shows them to that player only, and sells items through a validated, rate-limited remote:
-- ServerScriptService/Services/CoinService.luau
local Players = game:GetService("Players")
local Constella = require(game:GetService("ReplicatedStorage").Packages.Constella)
local Check = Constella.Network.Check
local PRICES = { Sword = 10, Shield = 50 }
local CoinService = { Name = "CoinService", Environment = "Server" }
function CoinService:Init(context)
local replicator = context.Cleanup:Add(Constella.Replication.Server.new("Coins"))
local network = context.Cleanup:Add(Constella.Network.Server.new("Shop"))
local coins = {}
context.Cleanup:Connect(Players.PlayerAdded, function(player)
coins[player] = Constella.State.new(100)
replicator:ShareWith(player, "Coins", coins[player]) -- visible to this player only
end)
context.Cleanup:Connect(Players.PlayerRemoving, function(player)
local balance = coins[player]
coins[player] = nil
if balance then
balance:Destroy()
end
end)
-- Clients may only send "Sword" or "Shield", at most twice per second.
network:Function("Buy", {
Schema = { Check.literal("Sword", "Shield") },
RateLimit = { Count = 2, Per = 1 },
}):SetHandler(function(player, item)
local balance, price = coins[player], PRICES[item]
if not balance or balance:Get() < price then
return false
end
balance:Update(function(current)
return current - price
end)
return true
end)
end
return CoinService
-- ServerScriptService/Main.server.luau
local Constella = require(game:GetService("ReplicatedStorage").Packages.Constella)
local app = Constella.App.new()
app:AddModules(script.Parent.Services) -- only this folder, never auto-discovered
app:Start()
game:BindToClose(function()
app:Stop()
end)
-- StarterPlayer/StarterPlayerScripts/Shop.client.luau
local Constella = require(game:GetService("ReplicatedStorage").Packages.Constella)
local coins = Constella.Replication.Client.new("Coins"):Get("Coins") -- read-only
coins:Observe(function(value)
print("Coins:", value)
end)
local buy = Constella.Network.Client.new("Shop"):Function("Buy", { Timeout = 5 })
local ok, bought = buy:Invoke("Sword") -- ok is false on a rejection, rate limit or timeout
print("Bought a sword:", ok and bought)
A complete demo game, with saved data, a coins UI and a shop, lives in src/server and src/client. Each module also has a short example in examples/.
Installation
Download (easiest). Get Constella.rbxm from the latest release. In Studio, right-click ReplicatedStorage → Insert from File… and choose the file. Then move the inserted Constella module into a Folder named Packages, so the path is ReplicatedStorage.Packages.Constella.
Tip
The package is named layfate/constella on Wally and zense/constella on pesde. The release download, the Wally package and the pesde package are built from the same tagged source.
Wally. Add Constella to wally.toml, then run wally install:
[dependencies]
Constella = "layfate/[email protected]"
pesde.
pesde add zense/constella
Build from source. Build the model file with Rojo, then insert Constella.rbxm into ReplicatedStorage.Packages:
rojo build default.project.json -o Constella.rbxm
Manual install (no tools)
- In Studio, create a Folder named
PackagesinReplicatedStorage. - Inside it, create a ModuleScript named
Constellaand paste insrc/shared/Constella/init.luau. - For every other file in
src/shared/Constella, add a child ModuleScript with the same name (without.luau). NetworkandDataare folders: give each one a ModuleScript holding itsinit.luau, with the rest of its files as children.
Development
rokit install # rojo, wally, selene, stylua
rojo serve dev.project.json # dev place: package, demo, examples and tests
selene src tests examples benchmarks # lint
stylua src tests examples benchmarks # format
Press Play in the dev place to run the full test suite and both integration tests. On every push and pull request, GitHub Actions checks formatting (StyLua), lint (selene) and strict Roblox types (luau-lsp), then runs the whole test suite on a real Roblox server through Open Cloud, including the live DataStore tests. See CONTRIBUTING.md for how tests are structured and what a pull request needs.
License
MIT © 2026 Zense. See LICENSE.