Saving progress
A player who quits at step 3 should come back to step 3. This page wires that up in four steps.
How the two halves talk
The kit carries its own RemoteEvent, created by the server module, so this costs you nothing in your networking layer.
The direction of authority matters:
- The client reports what it did. "I reached step 4."
- The server decides what happens next. "Go to step 4."
Steps the server ordered are not reported back, so a server decision never bounces back as a fresh report and you never get an echo loop.
Step 1 — turn replication on, client side
const tutorial = Tutorial.build(steps, { Replicate = true })
With Replicate = true, build yields until the RemoteEvent has replicated. Call it from somewhere that can yield, which a LocalScript body can.
Step 2 — start the bridge, server side
const TutorialServer = require(ReplicatedStorage.TutorialKit.Server)
TutorialServer:Start()
Do this during server boot, before any client can report. A client only reports once its own tutorial starts, but "before boot finishes" is a real window and reports that arrive first are lost.
Step 3 — save what gets reported
TutorialServer.PlayerStepAdvanced:Connect(function(player, step, previousStep)
DataService:Set(player, "TutorialStep", step)
end)
TutorialServer.TutorialCompleted:Connect(function(player)
DataService:Set(player, "TutorialDone", true)
DataService:Set(player, "TutorialStep", nil)
end)
previousStep is nil on the first step, and is mostly useful for analytics: it tells you which step players fall out of.
Step 4 — put returning players back
Players.PlayerAdded:Connect(function(player)
DataService:WaitForLoaded(player)
if DataService:Get(player, "TutorialDone") then
TutorialServer:Complete(player)
return
end
const saved = DataService:Get(player, "TutorialStep")
if saved then
TutorialServer:SetStep(player, saved)
end
end)
SetStep is the server saying "be here". The client obeys without reporting it back.
Note the order: check TutorialDone first. A player who finished should have their tutorial closed, not be dropped back onto their last step.
The client side of resuming
There are two ways to open on a saved step, and they suit different games.
Let the server drive it. The client calls tutorial:Start() at step 1 and the server's SetStep moves it. Simple, but the player may see step 1 flash first.
Read the step yourself before starting. No flash, but you need the value on the client:
const saved = PlayerDataClient:Get("TutorialStep")
tutorial:Start(saved)
Start with no argument opens the first step; with a number it opens that one.
Analytics
The same signals are a funnel, since you get every transition with where it came from:
TutorialServer.PlayerStepAdvanced:Connect(function(player, step, previousStep)
AnalyticsService:LogEvent(player, "tutorial_step", {
step = step,
from = previousStep,
})
end)
What this is not
A client can lie about which step it reached. These signals are progress tracking, not proof of work.
If finishing the tutorial grants something, validate on the server against something the server actually saw: the quest completing, the item being picked up, the NPC being talked to.
Malformed packets are parsed and dropped, so neither side ever sees a bad message. That protects you from corrupted traffic, not from a player who reports step 9 without playing steps 1 through 8.
Reading progress without a round trip
const step = TutorialServer:GetStep(player)
This is in-memory only and is cleared when the player leaves. It is a convenience for save hooks, not a substitute for your data store.
Driving the client without a Tutorial object
For the rare case where you need the server messages but not the runtime, TutorialKit.Client exposes them directly:
const TutorialClient = require(ReplicatedStorage.TutorialKit).Client
TutorialClient:Start()
TutorialClient.StepRequested:Connect(function(step)
print("server moved us to", step)
end)