Skip to content

Use Weave without Twill

The folder map and the twelve rules name no Twill function. Twill fills one layer, the foundation, and this guide replaces that layer.

It applies when a project uses another framework, a set of separate libraries, or plain modules.

NeedTwill suppliesWithout Twill
Boot in two phases, in a stable orderLifecycle.StartThe loader below
Reach another entryLifecycle.GetA require inside Start
Saved data with a session lockDataA data-store library, behind one service
Remotes with types, a rate, and a schemaNetA networking library, or a wrapper that validates
State sent one way to clientsReplicationA state library, or attributes set by the server
Cleanup by lifetimeScope, BagA cleanup library, or a table of connections per player

Each row is a need from a rule. A foundation that leaves a row empty leaves the rule to be kept by hand.

This replaces the body of Main. It requires every entry, runs each Init in name order, then runs each Start on its own thread.

ServerScriptService/Main
local ServerScriptService = game:GetService("ServerScriptService")
local folder = ServerScriptService.Services
local modules = {}
for _, child in folder:GetChildren() do
if child:IsA("ModuleScript") then
table.insert(modules, child)
end
end
table.sort(modules, function(first, second)
return first.Name < second.Name
end)
local entries = {}
for index, module in modules do
entries[index] = require(module)
end
for _, entry in entries do
if entry.Init then
entry.Init()
end
end
for _, entry in entries do
if entry.Start then
task.spawn(entry.Start)
end
end

For the client, use the same code in the Client local script with folder set to ReplicatedStorage:WaitForChild("Client"):WaitForChild("Controllers").

The loader has no player gate. Give the data service a Start that connects PlayerAdded, and have it call the other entries once a player’s data exists.

With no registry, an entry requires the other entry inside Start. By then every module has loaded, so two entries can use each other.

local CoinService: typeof(require(script.Parent.CoinService))
function ShopService.Start()
CoinService = require(script.Parent.CoinService)
end

This keeps R5: no entry requires another while it loads.

ToolPlace in Weave
Services and controllers written as plain modulesServices and Client/Controllers, booted by the loader.
FlameworkThe same folders in Studio, built from your source tree. Constructor injection takes the place of Lifecycle.Get. It needs roblox-ts and a sync tool.
An ECS library, such as Matter or jecsInside one feature. The entry owns the world and the schedule, and the systems live in Modules/<Feature>. Saved data still goes through its owner.
Fusion, React Lua, or VideComponents in Client/Modules/<Feature>, mounted by the controller, fed from replicated state. The interface is never the source of a value.
ProfileStore or another data libraryBehind one data service, which is the only code that touches a profile.
A schema compiler, such as Zap or BlinkThe schema takes the place of Shared/Remotes. It adds a build step.
Third-party packagesReplicatedStorage/Packages and ServerScriptService/ServerPackages.

All twelve rules, the folder names, the feature naming, the check, and the README. In the README, change the Foundation line under Status, and the example in R5.