Skip to content

Lifecycle

local MyService = {}
MyService.Priority = 50
function MyService.Init() end
function MyService.Start() end
function MyService.OnPlayerReady(player, data, bag) end
function MyService.OnPlayerRemoving(player) end
return MyService
-- Main
Lifecycle.Start(ServerScriptService.Services)

Hooks are called dot style and never receive self.

Every direct ModuleScript child of a folder passed to Start becomes a service. A module is discovered by being there, so adding one is adding a file.

Three cases are reported and skipped rather than raised:

CaseWhat happens
A nested folderWarned, with a note to pass it to Start as well.
A duplicate service nameWarned, and the second one is skipped.
A module that fails to loadReported with its error.

A module returning anything but a table is ignored without a word.

Services boot by Priority ascending, ties broken by name. A service without a Priority gets 100, so anything that must go first takes a lower number and anything that must go last takes a higher one.

Every Init finishes before any Start begins. Init runs in order on the calling thread; Start runs on its own thread per service, so one that yields does not hold up the rest.

Read another service in Start and after, not in Init. During Init the other service exists but has not set itself up.

A service marked Critical that raises in Init or Start locks the side.

On the server this means refusing everybody: every player present is kicked, and so is everyone who joins afterwards.

This server failed to start.
Critical service 'DataService' failed Init.
Please rejoin.

Only the first failure counts, and it is reported once. A service without Critical that raises is reported and stepped over, and the boot continues.

The pipeline is server only. On a client, PlayerAdded means somebody else joined, so there is nothing to hold.

A joining player waits behind the gate. When the gate releases them, every service is told in boot order, and each receives the player, whatever the gate released, and the bag that closes when they leave.

function MyService.OnPlayerReady(player, data, bag)
bag:Connect(player.CharacterAdded, onSpawn)
end

Leaving runs in reverse boot order, so a service reads what it depends on before that dependency has unwound. A player still held by the gate is skipped, though their bag still closes.

Start calls Scope.OwnPlayerClosing, so player bags close after the last OnPlayerRemoving rather than the moment the engine fires.

Lifecycle.SetPlayerGate(Data.Gate)

A gate receives the player and a release callback. Nothing sees the player until release is called, and calling it twice does nothing.

A gate that raises kicks the player rather than announcing them with no data:

This server failed to load your session. Please rejoin.

Install the gate during Init. Set after Start, it warns, and players who already joined were never held.

[Server] | [Client]

Discovers services in the given folders and runs the boot sequence.

function Lifecycle.Start(folders: Instance | { Instance })

Takes one folder or a list. Calling it a second time warns and does nothing.

Throws when given anything but instances.

[Server]

Installs the gate that holds joining players until whatever they need exists.

function Lifecycle.SetPlayerGate(gate: PlayerGate?)
type PlayerGate = (player: Player, release: (data: any) -> ()) -> ()

Pass nil to remove the gate. Throws on a client.

[Server] | [Client]

Returns a booted service by name.

function Lifecycle.Get<T>(name: string): T

The service table itself, not a copy. Safe from Start onwards, not before.

Throws when no service was discovered under that name.

[Server] | [Client]

Returns every registered service name, in the order they boot.

function Lifecycle.GetBootOrder(): { string }

A fresh list, safe to keep or reorder. The console reads this through twill boot.

[Server] | [Client]

Returns why this side refused to boot.

function Lifecycle.GetFailure(): string?

nil while the side is healthy.

Every field is optional. A module with none of them still counts as a service.

FieldTypeMeaning
Prioritynumber?Boot position, ascending. 100 when left out.
Criticalboolean?Whether failing to boot locks the side.
Init(() -> ())?Set up. Every Init finishes before any Start.
Start(() -> ())?Run. Each on its own thread.
OnPlayerReady((player, data, bag) -> ())?Server only. Called in boot order once the gate releases.
OnPlayerRemoving((player) -> ())?Server only. Called in reverse boot order.