Skip to content

Quick start

By the end of this page you will have a server that loads player data, boots a service, and prints a line when a player is ready.

This continues from Installing Twill. Both folders should already be in place.

Add a Script named Main in ServerScriptService.

ServerScriptService/Main
local Twill = require("@game/ServerScriptService/Twill")
-- Names the DataStore and the shape a new profile starts as. Fields missing
-- from an existing profile are filled in from the template on load, so adding
-- a field later needs nothing else.
Twill.Data.Configure({
Store = "PlayerData",
Version = 1,
Template = {
Coins = 0,
Visits = 0,
},
})
-- Holds each joining player until their data exists. Without this, services
-- would see players before their saved data arrived, and every one of them
-- would have to check for itself.
Twill.Lifecycle.SetPlayerGate(Twill.Data.Gate)
-- Finds every ModuleScript that is a direct child of Services, sorts them,
-- and boots them. Call it once, after everything above is configured.
Twill.Lifecycle.Start(script.Parent.Services)

Order matters here. Configure and SetPlayerGate both run before Start, because Start is what begins letting players through.

Add a ModuleScript named WelcomeService inside Services.

ServerScriptService/Services/WelcomeService
local WelcomeService = {}
-- Every hook is optional, so this one module is already a complete service.
-- It runs once per player, after their data has loaded.
function WelcomeService.OnPlayerReady(player, data)
-- `data` is the live profile table. Mutating it saves on its own:
-- there is no commit step, and no save call belongs in BindToClose.
data.Visits += 1
print(`{player.Name} has visited {data.Visits} time(s)`)
end
return WelcomeService

Press Play. The output shows:

Player1 has visited 1 time(s)

Stop and play again. The count goes up, because the profile was saved and reloaded.

If the count does not increase, the output also shows a warning that storage cannot be reached. Enable Studio Access to API Services in Game Settings → Security. Without it, Twill keeps player data in memory and forgets it when you stop.

The same Lifecycle runs on both sides, so your client code is discovered and ordered exactly the way your server code is.

Add a Folder named Client inside ReplicatedStorage, and a Folder named Controllers inside that. Put a ModuleScript in Controllers.

ReplicatedStorage/Client/Controllers/HudController
local HudController = {}
-- Start runs after every client module has finished Init, so by here it is
-- safe to reach for another one.
function HudController.Start()
print("client booted")
end
return HudController

Then add a LocalScript in StarterPlayerScripts to start them.

StarterPlayerScripts/Client
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Twill = require("@game/ReplicatedStorage/Twill")
Twill.Lifecycle.Start(ReplicatedStorage:WaitForChild("Client"):WaitForChild("Controllers"))

Press Play again and client booted joins the output.

The client gets discovery, ordering, and the two boot phases. It does not get the player pipeline, because on a client PlayerAdded means somebody else joined. Each side keeps its own boot list, so the two never interfere.

  • Player data with a template and a version number, ready for migrations.
  • A boot order you control, and a gate that means no service ever sees a player before their data exists.
  • One service on each side, discovered automatically. Adding another is one more ModuleScript in the same folder.

The Core Guides take each piece properly: Write a service for the server side, Write a controller for the client.

The folders on this page are the start of Weave, the layout Twill recommends for a whole project.