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 logo

Constella

A modular, strictly typed Luau toolkit for Roblox.
Use one utility on its own, or build your whole game on it.

CI License: MIT Version 0.3.2 Luau: strict Platform: Roblox

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

ModuleWhat it does
ArchitectureCore / AppServices and controllers with dependency-ordered Init/Start/Stop and injection
UtilitiesSignalCustom events with deterministic dispatch and error isolation
CleanupOwns connections, instances, threads and promises, and disposes them together
StateReactive values, computed state, batching and UI bindings
PromiseAsync chaining, cancellation, timeouts and retries
RandomWeighted rolls, loot tables and luck, with the math documented
DebugStructured logging and metrics, with almost no cost when switched off
Game systemsNetworkServer-owned remotes with validation, rate limits and payload caps
ReplicationServer→client State, only when the server explicitly shares it
ComponentBehaviour attached to CollectionService tags
PoolBounded object reuse for projectiles and effects
ZoneEnter/exit detection for players and parts
DataPlayer 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.

JobCommon choiceConstellaMain difference
Secure remotesPlain RemoteEvents; Zap, ByteNet or BlinkNetworkSecurity 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 dataProfileStore directlyDataBuilt on ProfileStore: adds migrations, template reconciliation, Changed signals, kick-on-session-loss and a shutdown budget that waits for every save
App structureKnitCoreExplicit apps instead of global singletons: dependency order, cycle and environment checks before any code runs, and rollback when startup fails
CleanupTrove, Janitor, MaidCleanupLevel with Janitor and about 6× faster than Trove for functions on a Roblox server, with half of Trove's memory (benchmarks)
SignalsGoodSignal, sleitnick/signalSignalCatches 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
Promisesevaera/promisePromiseA 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 stateFusionStateSmall reactive values with property bindings, not a UI framework. For building whole interfaces declaratively, Fusion does far more
ZonesZonePlusZoneSimple 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 componentssleitnick/componentComponentEach component gets its own Cleanup and an ancestor filter
Object reusePartCachePoolPools 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)
  1. In Studio, create a Folder named Packages in ReplicatedStorage.
  2. Inside it, create a ModuleScript named Constella and paste in src/shared/Constella/init.luau.
  3. For every other file in src/shared/Constella, add a child ModuleScript with the same name (without .luau).
  4. Network and Data are folders: give each one a ModuleScript holding its init.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.