Appearance
TypeTorch agent playbook
For AI coding agents (Claude Code, Codex, Cursor, Copilot, Aider and others). Follow it when the user says "migrate this project to typetorch", "set up typetorch", "convert to typetorch", "add typetorch" or similar.
You do every local step: read the project, plan, change code, build, check the payload. The user does every step that touches Roblox or secrets: API keys, signing keys, Creator Hub settings, publishing the place, deploying, approving. You finish with a numbered "What you need to do" list that tells them exactly how.
- Human guides: fresh setup, migrate (before/after code for every pattern), player data, hot assets.
- Today means it exists in the code now. Planned means it doesn't exist yet: never tell the user to use it.
Validated on 2026-10-05 with framework
7510856(0.2.0, no Flamework), transformer8ab4dbc(0.2.0), kernel422f95f(0.3.1), cli88f1766(0.6.0) and template6b628be: a throwaway roblox-ts project with two Flamework services, a controller, a raw RemoteEvent, aPlayers.PlayerAddedhandler,_Gstate and atask.spawnloop was migrated with this procedure (first on the Flamework-based toolchain, then moved to@typetorch/transformer) to a cleantypetorch build(dev and prod channel; only Folders and ModuleScripts; guards and DI ids generated, no Flamework left in the output), and itsstudio.project.jsonbuilt. The code samples in the guides it links (player data, characters, hot-asset tools, UI) compiled in that project. Nothing that needs keys was run.npm setup checked on 2026-10-05 with template
756e78c(every@typetorch/*package from npm: framework 0.2.0, kernel 0.3.1, transformer 0.2.0, cli 0.6.0, dev-server 0.2.0): a fresh clone built withbun install,bun run buildandbun run typetorch build, and so did a copy withoutscripts/packages.tsand its two scripts.Updated on 2026-11-15 from the code, not re-run: the toolchain steps now use
@typetorch/transformer0.3+ as the only compiler plugin (its own debug macros, norbxts-transform-debug), darklua for the production pack, and the newest npm versions instead of the template's pins.
Rules (read first, apply always)
- Never create, ask for, read, print or store a secret: Open Cloud API keys, signing key files (
~/.config/typetorch/keys/*), pairing codes, cookies, tokens. Don't open.envfiles or the file named byTYPETORCH_ENV_FILE. Don't put a key in chat, a file, a commit or a command line. If the user pastes a key into chat, tell them to revoke it in Creator Hub and make a new one. - Never act on Roblox. Don't publish or save a place, upload anything, or run:
typetorch upload,deploy(also not--dry-run: it reads the game's DataStore with the user's key),promote,rollback,approve,reject,pin,deployments,branch ls,settings ...,access push,kernel deploy(also not--dry-run; the one exception is a kernel update the user asked for, see "Updating the kernel in a game"),kernel restore,keys ...,assets sync|status,test --cloud,servers,report,alerts,backend setup,update,doctor(it probes the key and publishes a test message). Don't startremote-claude(typetorch dev), the analytics server or a tunnel. These go in the final list. - Never push. Local commits on a new branch are fine. Don't rewrite history, don't force anything.
- Don't edit TypeTorch itself:
node_modules/@typetorch/*, the reference../template, or any other TypeTorch checkout. Copy from them. - Don't weaken security to make something work: no new RemoteEvents,
loadstring,_Ghooks, disabled guards, or settings changes. - Don't change player data formats, DataStore names or keys. Follow Player data and flag everything for the user to test.
- Keep ids honest. Never invent a universe, place, group or user id. Use the user's, or the placeholder
1, and say so in the final list. - Only promise what exists today. On npm:
@typetorch/framework,transformer,kernel,cli(also installed astt) anddev-server; the template installs all five withbun install(npx @typetorch/cliworks too). Not on npm: the Studio plugin (TypeTorchSync.rbxmfrom its GitHub releases) and the backend (a checkout or its Docker image). Built and usable: kernel deploys that patch only the kernel into the live place (Luau Execution, no download; a copy withkernel deploy --place-filewhen the place doesn't allow saving through the API), the cloud test (typetorch test --cloud, automatic before prod deploys),onStopat server shutdown (kernel 0.3.2), the fleet API (typetorch servers,report,alerts) and the optionalAnalyticsEngine(framework 0.3.0; its backend is@typetorch/backend, a git repo, not on npm). The Studio loop:typetorch serve(CLI 0.11+) with the Studio plugin, live reload in a playtest (kernel 0.5.4+),typetorch studio ...andtypetorch mcpfor agents. The debug macros from@typetorch/transformer/macros(transformer 0.3+). Optional build tools: goblox (bun add -d goblox, CLI 0.16+) and darklua for the production pack (CLI 0.17+). Owners switching any server in place needs kernel 0.3.4 and framework 0.3.2 (both on npm). Dev access lists throughtypetorch access pushneed kernel 0.3.8 on the servers (the signed settings record; 0.3.6-0.3.7 read the old ConfigService key). Cross-server messages (TypeTorch.messaging),TypeTorch.servers(),TypeTorch.liveConfigand the loading screen signals need kernel 0.3.8.typetorch init(CLI 0.10) is the guided setup for a new game, and it repairs a game that is already set up. It publishes, deploys and asks for keys, so it is the user's to run, never yours: name it in "What you need to do". Planned: content packs, the typed asset map from files,typetorch test --unit,/tt grant/revoke. - No CI for the game, ever (the owner's rule: hosted CI is a supply-chain risk). Don't add
.github/workflows, actions, or any hosted CI to the game repo, and don't suggest them. Builds, cloud tests and deploys run on the user's machine. (TypeTorch's own packages are released by tag-triggered workflows; that is not a pattern for games.)
Step 1: Detect
Read the project. Change nothing yet. Write down what you find.
Is it roblox-ts?
package.jsonwithroblox-ts, atsconfig.json,.tssources, a*.project.json.- Plain Luau (only
.lua/.luau, Wally, Rojo): stop. Tell the user: "TypeTorch is roblox-ts only; there is no Luau framework. Port the game to roblox-ts first, or start a new game from the template (say 'set up typetorch' in an empty folder)." Do nothing else. - Empty folder, no game (the user said "set up typetorch"): follow Fresh setup path.
- Not a Roblox project: stop and say so.
- Plain Luau (only
Toolchain: lockfile (
bun.lock,package-lock.json,pnpm-lock.yaml,yarn.lock), roblox-ts and TypeScript versions,tsconfig.jsonpluginsandinclude,rokit.toml/aftman.toml/foreman.toml.Framework: Flamework (
@flamework/core,Flamework.addPaths/ignite,@Service,@Controller,Dependency<T>(),@flamework/networking,@flamework/components, customModdingmacros), Knit (KnitServer.CreateService,Knit.Start()), plain singletons, or something else (describe it).Entry points: every
*.server.tsand*.client.ts.Layout: the Rojo tree in
default.project.json; whichsrc/folders go where.Patterns (
git grepworks in PowerShell and bash; no output means none):shgit grep -nE "PlayerAdded|PlayerRemoving|CharacterAdded" -- src git grep -nE "RemoteEvent|RemoteFunction|OnServerEvent|OnClientEvent|InvokeServer|@rbxts/net|@flamework/networking" -- src git grep -nE "CollectionService|GetInstanceAddedSignal|@flamework/components" -- src git grep -nE "while \(true\)|task\.(spawn|delay|defer)|\bspawn\(|\bdelay\(|Heartbeat|RenderStepped|Stepped" -- src git grep -nE "_G\b|\bshared\." -- src git grep -nE "DataStoreService|ProfileStore|ProfileService|GetDataStore|MemoryStoreService" -- src git grep -nE "BindToClose|MessagingService|TeleportService" -- src git grep -nE "@rbxts/react|@rbxts/roact|@rbxts/vide|ScreenGui|PlayerGui|StarterGui" -- src git grep -nE "rbxassetid://|LoadAsset|:Clone\(\)|\.Clone\(\)" -- srcPlace content: what the old Rojo project puts in the place besides code (models,
.rbxm/.rbxmx,*.model.json, StarterGui, StarterPlayer scripts). Studio-built content isn't in the repo: ask, or note it.Ids: the universe, place and group ids, if the repo has them (
servePlaceIds, deploy scripts,mantle.yml, CI files). Don't guess.
Step 2: Plan and show the plan
Create MIGRATION_NOTES.md at the repo root with the plan, and show it in chat. Continue unless the user asked you to wait. Always ask before deleting files the user wrote by hand.
- Inventory: from Step 1, with file paths.
- Mapping: each old thing and its TypeTorch replacement (Step 4 tables).
- Order of work: the sub-steps of Steps 3 and 4, one commit each.
- Risks and decisions for the user: player data (always), Studio content and scripts that stay in the place, third-party networking (Zap, Blink, ByteNet), MessagingService topics (move them to
TypeTorch.messaging, see migrate), anything you can't convert. - User actions: filled in at the end (Step 7).
Log every step in MIGRATION_NOTES.md as you go: what changed, the build result, open questions.
Step 3: Add the TypeTorch toolchain
Work on a new branch: git switch -c typetorch-migration, from a clean tree (commit or stash the user's changes first; ask if unsure).
3.1 The template as reference
Every TypeTorch package comes from npm; the only checkout you need is the template, next to the game repo, to copy files from:
text
<workspace>/
my-game/ the user's game (this repo)
template/ https://github.com/typetorch/template (the reference: copy its files, never edit it)If it is missing, clone it from the game folder (ask first if your environment needs approval for downloads):
sh
git clone https://github.com/typetorch/template ../templateThe template decides the file layout, npm decides the versions. Copy its default.project.json, studio.project.json, scripts/build-info.ts and the shape of its package.json and tsconfig.json. Its version pins are behind npm today (framework 0.5.0, kernel 0.5.1, CLI 0.9.1, transformer 0.2.1, dev-server 0.2.3) and it still uses rbxts-transform-debug, so take the @typetorch/* versions from npm with @latest (3.3) and the compiler plugin from 3.4. Don't type versions from memory.
- Today: no Flamework.
@typetorch/frameworkand@typetorch/kernelare dependencies;@typetorch/transformer(the compiler plugin),@typetorch/cliand@typetorch/dev-serverare devDependencies, all from npm.Modding,Reflectandtcome from@typetorch/framework. - If the template you find still depends on
@flamework/core,rbxts-transformer-flameworkorfile:.typetorch/packages/*.tgzentries (an older checkout),git pullit first. - The template's
scripts/packages.ts(bun run packages, and thepackagesandpostinstallscripts) is an optional local override for framework, kernel or transformer changes that aren't on npm yet. Don't copy it unless the user asks for that; see fresh setup: unreleased changes.
3.2 Tools
Bun 1.3+ is the package manager. If the project uses npm, pnpm or yarn, run
bun install, then delete the old lockfile. Mixing package managers breaksrbxtsc.The CLI and the dev-server need no separate install: they are devDependencies (3.3). Run the CLI with
bun run typetorch <command>.Rokit:
rokit.tomlas in the template (keep the project's other tools; replace an old Rojo pin):toml[tools] rojo = "rojo-rbx/rojo@7.7.0-rc.1" lune = "lune-org/lune@0.10.5"Run
rokit install; checkrojo --versionprints7.7.0-rc.1inside the repo. Thenrokit add seaofvoices/darklua(it pins the newest darklua inrokit.toml):typetorch build,uploadanddeploypack the payload with it (CLI 0.17+) and warndarklua not found: this payload isn't minifiedwithout it.goblox (optional, only if the user asks):
bun add -d goblox.typetorch buildthen compiles with it instead of rbxtsc (CLI 0.16+); end thebuildscript withgoblox(3.3). It runs only@typetorch/transformerand ignores every other tsconfig plugin.
3.3 package.json
Merge the template's dependencies, devDependencies, overrides and these scripts into the game's (keep the game's own packages):
json
"build": "bun scripts/build-info.ts && rbxtsc",
"watch": "bun scripts/build-info.ts && rbxtsc -w",
"studio": "rojo serve studio.project.json",
"typetorch": "typetorch"buildmust end with the compiler,rbxtsc(orgobloxwhen the game uses it): the CLI appends-p <tsconfig>for prod builds, and runs the compiler directly, with a warning, when the script ends with something else.Dependencies:
@typetorch/framework,@typetorch/kernel,@rbxts/t(generated guards import it),@rbxts/trove,@rbxts/services. devDependencies:@typetorch/transformer,@typetorch/cli,@typetorch/dev-server, roblox-ts 3,@rbxts/types,@rbxts/compiler-types, TypeScript5.5.3(also inoverrides). Take the TypeTorch ones from npm, not the template's pins:shbun add @typetorch/framework@latest @typetorch/kernel@latest bun add -d @typetorch/transformer@latest @typetorch/cli@latest @typetorch/dev-server@latestDon't add
rbxts-transform-debug(the template still lists it):@typetorch/transformer0.3+ declares the same macros in@typetorch/transformer/macros. If the game already uses it, see 4.9.Leave out the template's
packagesandpostinstallscripts (the optional local override, 3.1). Apostinstallwithoutscripts/packages.tsmakesbun installfail.Remove packages the migration replaces (Knit,
@rbxts/net, all@flamework/*,rbxts-transformer-flamework) once no code imports them (Step 4.2).
Copy the build script and the Studio project, then install:
sh
mkdir -p scripts
cp ../template/scripts/build-info.ts scripts/build-info.ts
cp ../template/studio.project.json studio.project.json
bun installPowerShell: New-Item -ItemType Directory -Force scripts and Copy-Item <from> <to> instead of mkdir -p and cp.
The template's studio.project.json lists the kernel's server files one by one and misses RemoteDebug, Bundle and LiveDebug: a playtest then warns TypeTorchKernel.<name> is missing and runs without them. In the copy, add the three under ServerScriptService.TypeTorchKernel as node_modules/@typetorch/kernel/place.project.json maps them, with the node_modules/@typetorch/kernel/ prefix.
In the copied scripts/build-info.ts, change the one import line it writes into src/shared/build.ts:
ts
'import { $compileTime, $git } from "@typetorch/transformer/macros";',(the template's copy says "rbxts-transform-debug"; without that package bun run build and watch fail on build.ts). typetorch build writes build.ts itself and picks the import on its own (CLI 0.12.2+).
Check: node_modules/@typetorch/framework/out/init.luau, node_modules/@typetorch/kernel/place.project.json, node_modules/@typetorch/transformer/out/index.js and node_modules/@typetorch/transformer/macros.d.ts exist, and bun run typetorch --help prints typetorch <version>: hot-swap roblox-ts game code on live Roblox servers.
3.4 tsconfig.json
Keep the project's options; take these from the template:
"include": ["./src/**/*.ts"]. Without it rbxtsc compilesscripts/*.tsand fails (Cannot find name 'console')."experimentalDecorators": true,"rootDir": "src","outDir": "out"."typeRoots": ["node_modules/@rbxts", "node_modules/@typetorch"](dropnode_modules/@flamework)."types": ["types", "compiler-types"]. Without it the@typetorchtype root pulls in the kernel package: TS2688.plugins:{ "transform": "@typetorch/transformer" }first. Removerbxts-transformer-flameworkandrbxts-transform-debug(transformer 0.3+ expands its macros). Keep other transformers the project uses after it (goblox ignores them).
3.5 The payload project
The game's code becomes a payload: a roblox-ts Model project. Keep the old project for reference:
sh
git mv default.project.json legacy.project.json
cp ../template/default.project.json default.project.json- The template's file is a
ModelwithServer(out/server),Shared(out/shared),Client(out/client) andinclude(withnode_modules/@rbxts, and@typetorch/frameworkonly, never the kernel). - Sources must be in
src/server,src/shared,src/client. Move other folders withgit mvand fix imports. - Note in
MIGRATION_NOTES.mdthe non-code content oflegacy.project.jsonthat must now live in the place.
3.6 .gitignore
Add what's missing:
gitignore
node_modules/
out/
include/
*.tsbuildinfo
src/shared/build.ts
.typetorch/
.payload.gen.project.json
.tsconfig.typetorch.json
.env
.env.*If out/ or include/ were committed: git rm -r --cached out include. Delete Flamework leftovers: flamework.build, flamework.json, include/flamework.
3.7 typetorch.json
At the repo root:
json
{
"project": "my-game",
"universeId": 1,
"placeId": 1,
"creator": { "groupId": 1 },
"defaultBranch": "prod",
"branches": { "main": "prod" },
"channels": { "prod": "prod" },
"members": {},
"devBadgeId": null,
"approval": "prod",
"kernel": "node_modules/@typetorch/kernel"
}universeId,placeId,creator: the user's ({ "groupId": … }or{ "userId": … }, the experience owner), or the placeholder1.branches: map the repo's default git branch (mainormaster) toprod.members:{}unless the user gave user ids; each is"owner"or"dev"(an old"admin"counts as dev). The experience's creator (or the owning group's owner) is always an owner. Servers seemembers,revokedanddevBadgeIdonly after the user runsbun run typetorch access push(kernel 0.3.8+; it signs with the prod keys): put it in the user's list whenever you add or change them.approval: "prod": dev deploys go out at once, prod ones wait for the user's y/N.- Optional (kernel 0.3.7+), only after the dev soak in step 11 shows a need:
"health": { "errors": 3, "window": 30 }: a server rolls a new build back aterrors(1-100) errors from it withinwindowseconds (5-300) of start."rollback": falseturns that off;"prod"/"dev"override per channel."autoRollback": { "failedPct": 20 }:deploy --waitrolls the branch back at this % of failed servers (1-100).- Raise them only to match errors the user can't fix yet; say so in MIGRATION_NOTES.md.
- Never copy the template's
signingPublicKeys,keyAssetIdorfallbackPublicKey: they belong to the template's game.typetorch keys initwrites the user's.
3.8 Boot modules
ts
// src/server/boot.ts
import { startServer, type ServerKernel } from "@typetorch/framework";
import { BUILD } from "../shared/build";
export function boot(kernel: ServerKernel) {
return startServer(kernel, { modules: [script.Parent!.FindFirstChild("services")!], build: BUILD });
}ts
// src/client/boot.ts
import { startClient, type ClientKernel } from "@typetorch/framework";
import { BUILD } from "../shared/build";
export function boot(kernel: ClientKernel) {
return startClient(kernel, { modules: [script.Parent!.FindFirstChild("controllers")!], build: BUILD });
}- Every ModuleScript under the
modulesfolders is required: their top-level code must have no side effects. bun run buildnow writessrc/shared/build.ts(gitignored) and compiles.
Check: run bun run build, then commit (git add -A, git commit -m "TypeTorch toolchain").
- Knit or plain projects: it passes (old code still compiles).
- Flamework projects: it fails on every
@flamework/coreimport with "You can only use npm scopes that are listed in your typeRoots". Expected: the toolchain no longer has Flamework. Note it in the log; Step 4.1 and 4.2 make it pass. bun run typetorch buildfails on the old entry scripts until Step 4.1.- Run the compiler only through
bun run buildorbunx rbxtsc. In a Bun project on Windows,npx rbxtscruns an unrelated placeholder package.
Step 4: Migrate the code in small verifiable steps
One sub-step per commit. After each: bun run build must pass, then log it in MIGRATION_NOTES.md. (Flamework projects: do 4.1 and 4.2 together; the build passes once no @flamework/* import is left.)
Flamework projects: run the codemod first. On a clean tree after Step 3, bun run typetorch migrate --from flamework --dry-run, then without --dry-run: it makes 4.1, 4.2 and the networking part of 4.4 (call sites stay, through createFlameworkCompat) and writes typetorch-migrate-report.md. Commit that as one step, then work through the report's flagged items with 4.3 and the rest of this step; keep the report's counts in MIGRATION_NOTES.md. Local only, no keys. See Coming from Flamework.
4.1 Entry points
Move what each *.server.ts / *.client.ts does into modules, then delete the script (git rm). Delete Flamework.addPaths(...), Flamework.ignite(), Knit.Start() and hand-written bootstrappers. A Script or LocalScript left in the payload fails typetorch build with "the payload may hold only Folders and ModuleScripts".
4.2 Services and controllers → modules
| Old | TypeTorch |
|---|---|
Flamework @Service() / @Controller() | @Service() / @Controller() from @typetorch/framework; the class extends Module; a constructor calls super() |
Flamework OnInit, OnStart, OnTick, OnPhysics, OnRender | the same names from @typetorch/framework |
Flamework Dependency<T>() | constructor injection in a module; Dependency<T>() from @typetorch/framework everywhere else (methods, plain classes, command handlers), from onInit on |
| a dependency cycle (A injects B, B injects A) | Lazy<T> on one side (a field = Lazy<T>(), or a constructor parameter with transformer 0.2.1+), or Dependency<T>() in a method |
Modding, Reflect, t from @flamework/core | the same names from @typetorch/framework |
custom decorators (Modding.createDecorator, @metadata flamework:parameters injectable) | Modding.createDecorator from @typetorch/framework, @metadata typetorch:parameters injectable |
Flamework @Component / @flamework/components | a module using observeElement(this.trove, tag, …) or @rbxts/observers |
Flamework Flamework.createGuard<T>(), Flamework.id<T>() | t guards written by hand, or a user macro (transformer README "Migrating from Flamework"); note it for the user |
Knit CreateService({ Name, Client, KnitInit, KnitStart }) | @Service() class; KnitInit → onInit, KnitStart → onStart, Client → network leaves |
Knit Knit.GetService("X") / GetController | constructor injection, or Dependency<X>() in a method |
| singleton module with top-level state | a @Service() / @Controller() class; state on the instance or in persist |
| shutdown cleanup | onStop (reverse order; the kernel gives a stopping generation 5 s) and the trove |
- Server modules go in
src/server/services/, client modules insrc/client/controllers/(subfolders are fine); kebab-case names (coin.service.ts). - Import only from
@typetorch/framework:Module,Service,Controller,OnInit,OnStart,OnStop,OnTick,OnPhysics,OnRender,OnPlayerAdded,TypeTorch,Dependency,Lazy,createNetwork,observePlayers,observeElement,isRealFrame,popIn,popOut,bump,PopupQueue,hotAsset,setNetworkLimits, andModding,Reflect,tfor your own macros and decorators. - If the project uses Flamework, the migration is mostly a swap: the decorators, constructor injection,
Dependency<T>()and the lifecycle interfaces keep their names, so change the imports to@typetorch/framework, addextends Moduleandsuper(), and do the swap-safety pass (4.3). The rest is the toolchain (Step 3),createNetworkfor@flamework/networking(4.4) and observers for components.Dependency<T>()is the cycle breaker (withLazy<T>) and the way plain classes reach modules; it works fromonIniton, so move every call in a constructor, a field initializer or at the top level of a module (const X = Dependency<X>()in a command file) into a method oronInit. Keepimport typefor the class. Then:- no file under
src/imports@flamework/*:git grep -n "@flamework/" -- srcprints nothing; - remove
@flamework/*andrbxts-transformer-flameworkfrompackage.json(bun remove ...); - type ids changed format (
@typetorch/framework:decorators@Service,server/services/x@X): anything the game saved under Flamework ids (Flamework.id<T>()) won't match. Flag it under data.
- no file under
- No work in constructors (
this.troveandthis.ctxarrive right after). onInitruns in dependency order and may yield briefly (a swap allows about 20 s for the whole generation to be ready, a new server's boot only about 6 s); move slow loading toonStart, which is spawned per module inside its trove.
4.3 Swap-safety pass (every module)
Every module must be stoppable and restartable: a deploy stops it and starts a fresh copy in the same server.
- Everything in the trove:
this.trove.connect(signal, fn),this.trove.add(instance),this.trove.add(network.server.x.y.on(...)),this.trove.add(TypeTorch.onSwapOut(...)). No cleanup touches a trove: a function added to a trove must never calltrove.remove,addorextend(Trove throws "Cannot call trove.remove() while cleaning"; before framework 0.5.2 that aborts the whole generation stop and leaves the old generation alive). Delete lines likethis.trove.add(() => this.trove.remove(child)): a child fromtrove.extend()is cleaned by its parent anyway. Audit:git grep -nE "trove\.add\(\(\) =>.*\.(remove|add|extend)\(" -- src, then read every cleanup function by hand. - No module-level state or side effects. Constants may stay. Per-generation state goes on the instance. State that must survive a swap goes in
this.ctx.persist("key.v1", () => init), plain data only (tables, Maps, Sets of strings, numbers, booleans, Players); never functions, class instances, Promises, threads, connections, charm atoms or Instances the generation made. Version the key. Per-player maps (cooldowns, states, sessions):this.ctx.playerState("key.v1", (player) => init)(get/set/has/delete, persisted, removed on a real leave).this.ctxandDependency<T>()only fromonIniton: a field initializer (private state = this.ctx.persist(...)) or a constructor fails, becausectxis attached after construction. Declare the field and assign it inonInit. - Players through
onPlayerAdded(player, playerTrove)orobservePlayers(this.trove, …), neverPlayers.PlayerAdded.Connect. They replay everyone on every swap, so join handlers must be idempotent: guard one-time effects (join rewards, welcome popups, "joined" analytics) with a persisted set. Work for real leaves only:this.trove.connect(Players.PlayerRemoving, …)(aplayerTroveis also cleaned on every swap). - Tags and characters through observers:
observeElement(this.trove, tag, (instance, elementTrove) => …), or@rbxts/observerswith its stop function in the trove (this.trove.add(Observers.observeCharacter(…))), or (characters, no extra package)player.CharacterplusplayerTrove.connect(player.CharacterAdded, …). - No global connections or loops outside troves. A loop directly in
onStartis fine. Elsewhere:this.trove.add(task.spawn(() => { … })). PreferonTickfor per-frame work. - No
_Gorshared: constructor injection, orpersist. task.spawn/delay/deferonly through the trove:this.trove.add(task.delay(5, fn)). Replace deprecatedspawn,delay,waitwithtask.*.- Instances in the world that a module creates go in its trove (or an observer's cleanup). Libraries with module-level setup (connections, ScreenGuis, steppers, pending Promise chains created when the module is required; Vide's Heartbeat stepper and TopbarPlus are known ones) leak one copy per swap. Check each package's top level. Fix: drive it from the trove (
this.trove.connect(RunService.Heartbeat, (dt) => vide.step(dt))), or load it once outside the payload (a place script requires it fromReplicatedStorage.lib, or the first generation clones it there). See migrate rule 9. - Looped animations and cutscenes stop on every client swap. Persist plain data (
{ id, animationId, startedAt },{ startedAt, endsAt }) and resume in the next generation (track.TimePosition = elapsed); derive idles from synced state. Flag minigames for the user: persist their inputs, restart the rest. See migrate section 7. - Shutdown: kernel 0.3.2+ runs every module's
onStopwhen the server shuts down, but not when it shuts down in the middle of a swap (no generation is running then). SoonStopis for short extras, never the only place player data is saved: data goes through the library in the place (4.6), or is written as it changes. Removegame.BindToClosehandlers that only did cleanup; any that must stay bind at most once per server (a persisted flag). On a swap,TypeTorch.onSwapOutruns first, before anyonStopor trove cleanup: last-resort work (save intopersist, flush) goes there.
4.4 Networking
One
src/shared/net.ts:tsimport { createNetwork, type ProperReturns } from "@typetorch/framework"; interface ClientToServer { shop: { buy(itemId: string): void; price(itemId: string): ProperReturns<number> } } interface ServerToClient { shop: { purchased(itemId: string): void } } export const network = createNetwork<ClientToServer, ServerToClient>();Server:
this.trove.add(network.server.shop.buy.on((player, itemId) => …)); requests:this.trove.add(network.server.shop.price.handle((player, itemId) => [100]))([value]or[false, "reason"]); sends:network.server.shop.purchased.fire(player, id),fireAll,fireExcept,fireList,fireUnreliable.Client:
network.client.shop.buy.fire(id);this.trove.addPromise(network.client.shop.price.invoke(id).then(…));this.trove.add(network.client.shop.purchased.on((id) => …)).Delete every RemoteEvent/RemoteFunction/UnreliableRemoteEvent the game created, in code, the Rojo project or the place notes. RemoteFunctions become request leaves.
Template literal types can't be guarded: use
string. Keep game checks (distance, ownership, cooldowns) in handlers.@rbxts/net,@flamework/networking, Zap, Blink, ByteNet: convert tocreateNetwork. Flamework names:connect→on,setCallback→handle,broadcast→fireAll,except→fireExcept,predict→emit(runs the localonhandlers, no traffic),invokeWithTimeout(seconds, …)keeps its name; check the unit (seconds, 0.5 to 120: a Flamework call with5000meant milliseconds by mistake). The codemod doesn't change the arguments: audit every call withgit grep -n "invokeWithTimeout(" -- srcand turn values above 120 into seconds. A leaf's default timeout:setNetworkLimits({ "x.y": { timeout: 30 } })insrc/shared/net.ts(the client reads it).A big Flamework network can stay on
createFlameworkCompat(the codemod's default) and move tocreateNetworklater (typetorch migrate --from flamework --net native);typetorch buildreminds you while it is in use.
4.5 UI
- Code-built UI: build it in a controller;
this.trove.add(screenGui). - Studio-built UI stays in the place (StarterGui): tag what code touches (
Element:<Area>.<Name>), drive it withobserveElement. The payload can't carry ScreenGuis. - charm atoms live per generation; persist their plain values if they must survive a swap. charm-sync: the recipe in State with charm (server atoms from persist, a
createNetworkleaf, hydrate request in onStart). - React/Roact/Vide: mount in a controller,
this.trove.add(() => root.unmount()). popIn/popOut/bump(UIScale, never tweenedSize); onePopupQueuefor modals.- Update notices:
TypeTorch.onUpdatePendingfires before every hot swap, and players stay in the server. Never show "migrating server" there; show "updating in N s" or nothing, and announce the result fromTypeTorch.startInfoinonStart(Runtime API).
4.6 Player data (always flag it)
Player data is game code. The swap-safe pattern (Player data): the library lives in the place (for example ServerStorage.Packages.ProfileStore) with a small host Script that requires it first; session handles live in persist; load on join (re-attach after a swap), release on a real leave.
| The project uses | Do |
|---|---|
| ProfileStore / ProfileService | rewrite the data module as the guide's DataService for that library (same store names and keys, a _dev name for non-prod channels; loads, saves and releases as DataHost jobs, or with TypeTorch.runDetached on kernel 0.3.8+: the guide's two options); the place copy and the DataHost Script are user steps (Studio) |
| plain DataStores, small per-player values | keep them in the payload: read once per join, UpdateAsync on change, pending writes in persist (like the template's BestService) |
| anything else (DataStore2, Lapis, custom sessions) | wrap it in a @Service() unchanged, and write in MIGRATION_NOTES.md that it must move to the place and follow the pattern before prod |
Never rename stores or change the data shape. Always put "test data across two deploys on a dev branch" in the user's list.
- The cloud test: keep the guide's line
if (Workspace.GetAttribute("TypeTorchTest") === true) return undefined;inprofileStore(). The place'sDataHostnever runs in the test, so without itonInitwaits 10 s, fails, and every prod deploy is refused. Wrapped libraries follow the same rule: never wait on a place script in the test. - Developer products:
ProcessReceiptrecords the PurchaseId in the player's profile together with the grant and returnsPurchaseGrantedonly once a save holds it (the guide'sgrantOnceandReceiptService). Never only inpersist. Put "buy a product while a deploy runs" in the user's list.
The cloud test runs game code against the live game's data. Every prod deploy boots the build in a headless Luau Execution task: onInit and onStart of every module, with the real DataStores, MemoryStores, MessagingService and HTTP, no players, no place scripts. TypeTorch.channel is dev there (CLI 0.7.3+), so stores split by channel point at dev data; unsplit stores don't. The AnalyticsEngine sends nothing from the test (framework after 0.3.2). workspace:GetAttribute("TypeTorchTest") is true there. Guard with it, and list each guard in MIGRATION_NOTES.md:
- global resets and one-time jobs (season rollovers, leaderboard wipes, shared-key migrations);
- raw MessagingService publishes (cross-server announcements, "server started" pings;
TypeTorch.messaging.publishalready does nothing there); - "server started" rows in DataStores or MemoryStores, webhooks, the game's own HTTP APIs;
- waits on place scripts.
Guard only side effects; the test is useful because it boots the real code. Details: migrate: the cloud test runs your game code.
4.7 Place content, assets and scripts that stay
- The place keeps: the kernel, maps, terrain, lighting, StarterGui layouts, StarterPlayer/StarterCharacter scripts, third-party Luau systems, the data library and its host Script. They change with a place publish and a restart.
- Hard-coded asset ids keep working (owned by or shared with the experience owner).
- Hot assets (optional; only if the user asks, or mention it in the report): code that clones templates can use
hotAsset("tools/sword", template)(the second argument keeps today's template as the fallback). Marking templates withTypeTorchAssetandtypetorch assets syncare user steps. See Hot assets. - Never suggest
typetorch kernel deploy --replace-placefor a place with Studio content. The kernel goes in withkernel deploy --install(a user step, Step 7; with--place-file <copy>when the place doesn't allow saving through the API) or by hand in Studio.
4.8 Analytics (optional)
TypeTorch's own analytics (Analytics) is optional: an AnalyticsEngine created on the server and the client logs sessions, devices, tech health, zones and new players' first sessions; game code adds step, purchase, currency, state and track. The engine starts on the first new AnalyticsEngine(): create it in a low-loadOrder module's onInit, not in a helper that first runs on a game event (an idle server would send nothing). Add it only if the user asks (or mention it in the report). Keep an existing analytics SDK as it is. Its backend (a DuckDB server or Cloudflare Basin, tokens, the settings key) is a user step.
4.9 Logging (optional)
$print, $warn, $error, $assert and $dbg from @typetorch/transformer/macros add [src/file.ts:line] in dev builds; prod builds drop $print/$warn/$dbg and keep $assert/$error without the position. Plain print/warn keep working (and stay in prod builds).
A game that already imports them from rbxts-transform-debug: remove that plugin from tsconfig.json (the transformer expands calls through its declarations too), rewrite the imports to @typetorch/transformer/macros (in src/ and scripts/build-info.ts), then bun remove rbxts-transform-debug. bun run build after each. Details: migrate: Debug macros.
Step 5: Run local checks
All local; no key needed.
sh
bun run build
bun run typetorch buildbun run buildhas no errors.bun run typetorch buildprintsbuilt <artifact id> (branch ..., channel ..., <git branch>@<commit>, ...)and.typetorch/payload.rbxm <size> <n> modules. It fails, naming the path, if the payload holds anything but Folders and ModuleScripts.The guards were generated:
out/shared/net.luaucallscreateNetwork(witht.guards (for examplet.strictArray(t.string)).createNetwork()with no arguments means the transformer isn't running.The pattern scan prints nothing (or only lines you checked and explain in the notes):
shgit grep -nE "Players\.PlayerAdded\.Connect|PlayerRemoving\.Connect\(" -- src git grep -nE "new Instance\(\"(Remote|UnreliableRemote)(Event|Function)\"\)" -- src git grep -nE "_G\b|\bshared\." -- src git grep -nE "@flamework/|Flamework\.(ignite|addPaths)|Knit\.Start" -- src git grep -nE "^\s*task\.(spawn|delay|defer)\(|[^.]\bspawn\(|[^.]\bdelay\(" -- srcCommit, then
bun run typetorch buildagain: the id has no-dirty. Also try a prod build:bun run typetorch build --branch prod("prod channel: $print/$warn removed").The Studio project builds:
rojo build studio.project.json -o .typetorch/studio-check.rbxl, then delete that file.Optional, only when the user has Studio open with the TypeTorch plugin:
bun run typetorch serve --detach, thenbun run typetorch studio status --json,studio play,studio logs(look for[TypeTorch]lines and errors) andstudio stop; or the same throughtypetorch mcp.serve status --jsonlists compile errors. Plainserveruns no keys; never add--remoteor--claude, and stop it (serve stop) when done.
Fix, rebuild, log.
Step 6: Stop at user actions
Don't do these; list them in Step 7:
- create the group, the experience, an API key; change Creator Hub or Game Settings;
- write a key or token anywhere; run
typetorch keys ...,doctor,deploy,upload,approve,promote,rollback,pin,settings ...,access push,assets sync|status,kernel deploy,kernel restore,deployments,branch ls,test --cloud,servers,report,alerts,backend setup,update; - publish a place; edit the place in Studio (kernel install, data library,
TypeTorchAssetmarks); - start
remote-claude, the analytics server or a tunnel (includingtypetorch serve --remoteor--claude); press the Studio plugin's Upload or Publish to dev; write the analytics settings; - push to a remote; add GitHub Actions or any CI workflow.
Step 7: Finish with "What you need to do"
End with this list, filled in for the project (drop what doesn't apply). Also append it to MIGRATION_NOTES.md.
markdown
## What you need to do
1. **Fill in the ids** in `typetorch.json` (replace the placeholder `1`s): `universeId`, `placeId`, and
`creator.groupId` (or `creator.userId`). Creator Hub > Creations > your experience > "..." > Copy Universe ID /
Copy Start Place ID. Optional: other people's user ids in `members` (`"owner"` or `"dev"`; you, the experience
owner, are always an owner), then `bun run typetorch access push` after step 5, and again after every change to
`members`, `revoked` or `devBadgeId` (it needs the signing keys). Servers see the lists only after that, on kernel
0.3.8+; `deploy` and `doctor` warn when they were never pushed or changed since.
2. **Experience settings** (Studio > File > Game Settings > Security, as the owner): Allow HTTP Requests on; Enable
Studio Access to API Services on. Optional: Allow Mesh / Image APIs (Claude images), Allow Loading Third Party
Assets (Toolbox inserts).
3. **Create an Open Cloud API key** (Creator Hub > Open Cloud > API Keys, as the group owner or a member allowed to
upload and publish for the group), for this experience:
- `asset:read`, `asset:write`; Luau Execution `universe.place.luau-execution-session:read` and `:write` (the cloud
test that runs before every prod deploy, hot assets, doctor's place check)
- `universe-messaging-service:publish`
- DataStore `universe-datastores.objects:read`, `:create` and `:update` (the shared deploy number: every machine
takes `#seq` from the game's DataStore; and the signed settings record: `access push`, `backend setup`,
`settings ...`)
- place publishing (`universe-places` write; the CLI says `universe.place:write`), only when `kernel deploy`
publishes a file: `--replace-place` (a new place), `--place-file`, `kernel restore <file>`
Set an expiry and, if you can, an IP allowlist. No `universe:write` / `universe:read` (TypeTorch keeps nothing in
ConfigService since kernel 0.3.8). Don't ask for `legacy-asset:manage` (place downloads): Creator Hub doesn't offer
it for API keys today. Deploys work without it: `kernel deploy` patches the place through Luau Execution when the place allows saving through
the API, and takes a copy of the place otherwise (`--place-file`, see "Updating the kernel in a game").
4. **Put it in the game repo's `.env`:** the line `OPENCLOUD_API_KEY=<key>` (gitignored; CLI 0.9 reads the keys from there:
`OPENCLOUD_API_KEY`, and `TYPETORCH_API_KEY` and `TYPETORCH_ADMIN_TOKEN` for the backend). Never commit it or paste it
into chat.
5. **Check:** `bun run typetorch doctor`. Expect `ok` for the tools, `typetorch.json`, each key and the scopes you added.
6. **Prod signing keys:** `bun run typetorch keys init`, `bun run typetorch keys init --fallback`, commit
`typetorch.json`. Back up `~/.config/typetorch/keys/<universeId>.key`, `.fallback.key` and the game repo's `.env` offline,
and plan a rotation drill ([Prod signing: back up and drill](https://github.com/typetorch/docs/blob/main/guides/prod-signing.md#back-up-and-drill)).
7. **Put the kernel in the place** (needs a place publish and a restart):
- empty or new place: `bun run typetorch kernel deploy --dry-run`, then
`bun run typetorch kernel deploy --replace-place --yes`;
- place with Studio content (never `--replace-place`, it wipes the place): `bun run typetorch kernel deploy --dry-run
--install` patches the live place through Luau Execution, with no download, when the place allows saving through the API
(Creator Hub > Permissions: "Allow place to be updated using Save Place API"; off by default for places made in Studio).
Read the summary (only the kernel folders and the kernel's settings may change), then the same command without
`--dry-run` publishes after a y/N. Undo: `kernel restore --version <the version before>`. Otherwise: File > Download a
Copy (a binary `.rbxl`), note its place version, and run `bun run typetorch kernel deploy --dry-run --install
--place-file <file> --base <version>`; the same command without `--dry-run` publishes after a y/N, and
`kernel restore <file>` undoes it. (Or
by hand: `kernel deploy --dry-run`, open `.typetorch/place.rbxl` in Studio, copy
`ServerScriptService.TypeTorchKernel`, `ReplicatedStorage.TypeTorchKernelShared` and
`ReplicatedFirst.TypeTorchKernelClient` into your place, File > Publish to Roblox.) The kernel waits idle until
the first deploy.
- `ServerScriptService.LoadStringEnabled`: keep it **off** in this game. Turn it on only in a test place where you
want remote-claude's `run_luau` (`kernel deploy --loadstring`, CLI 0.7.5+). `kernel deploy` patches leave it as it
is (older CLIs, up to 0.7.2, turn it on: check it in Studio); `doctor` reports it.
- then, in Studio, remove the old scripts listed in MIGRATION_NOTES.md and publish, just before the first deploy.
8. **Player data** (if listed in MIGRATION_NOTES.md): in Studio put the library at `ServerStorage.Packages.<Name>` and
add the `ServerScriptService.DataHost` Script from the Player data guide; publish.
9. **Test locally in Studio:** install the TypeTorch Studio plugin (`TypeTorchSync.rbxm` from
github.com/typetorch/studio-plugin/releases into Studio's plugins folder), run `bun run typetorch serve`, open the
place (the plugin attaches by itself), Play. Server > Status shows "Studio: local payload". Saves reload the running
playtest (kernel 0.5.4+); the dev menu's Reload tests a swap.
10. **Check the kernel live:** join the game; F9 > Server shows `[TypeTorch] kernel <version> (API 1) on a public
server, branch prod, signed deploys only (keys: key asset)`. `/tt status` answers.
11. **Dev branch:** `git switch -c dev`, `bun run typetorch deploy`, then `/tt new dev` in game. Earn some data, deploy
again twice while playing, rejoin: nothing lost. Developer products: buy one while a deploy runs; one grant, also
after a rejoin. `bun run typetorch test --cloud` passes on the dev build (prod deploys run the same test).
After each deploy, ask the user for the dev menu's Server > Status line "Health window: n/3 errors" (errors from
the new build in its first 30 s). 3 rolls every server back: fix those errors, or set `typetorch.json` `"health"`
above the count (3.7) and rebuild.
12. **First prod deploy:** a game with live players goes through the
[go-live checklist](https://github.com/typetorch/docs/blob/main/guides/go-live-checklist.md) first (a copy of the
game, a dev-branch soak, a planned cut-over). Merge into `main`, stay in the game, `bun run typetorch deploy` (the
cloud test runs, then y/N, then signed). Expect `deployed #N prod@<commit> -> <artifact id>`; the dev menu's
Artifact tab shows two verified badges.
13. **Rollback drill:** `bun run typetorch rollback --branch prod`, check `bun run typetorch deployments`, deploy again.
14. Optional: [live servers and alerts](https://github.com/typetorch/docs/blob/main/guides/fleet-and-alerts.md)
(`typetorch backend setup`, then `servers`, `report`, `alerts`, and automatic rollback after bad deploys);
[analytics](https://github.com/typetorch/docs/blob/main/guides/analytics.md);
[hot assets](https://github.com/typetorch/docs/blob/main/guides/hot-assets.md);
[remote-claude](https://github.com/typetorch/docs/blob/main/guides/remote-claude.md) (in a test place: it needs
`LoadStringEnabled` for `run_luau`).Final report template
markdown
# TypeTorch migration report
**Result:** builds: <yes/no> · branch `typetorch-migration` · <N> commits · artifact `<id from typetorch build>`
## What I changed
- Toolchain: <bun, rokit, package.json, tsconfig, default.project.json (payload), studio.project.json, typetorch.json, .gitignore>
- Entry points: <old scripts> → `src/server/boot.ts`, `src/client/boot.ts`
- Modules: <N services, M controllers> (<framework> → TypeTorch); Flamework imports left: <none>
- Networking: <K remotes> → `src/shared/net.ts` (<leaves>)
- Swap safety: <player handlers, loops, globals, tags, characters>
- UI: <...>
- Player data: <pattern applied / wrapped / not present>
## Checks
- `bun run build`: <pass/fail>
- `bun run typetorch build`: <pass/fail>, <n> modules, clean id: <yes/no>; prod build: <pass/fail>
- Guards generated: <yes/no>
- Pattern scan: <clean / items with reasons>
- `studio.project.json` builds: <yes/no>
## Needs your decision
- Player data: <what it does, the risk, what to test>
- Place content and scripts that stay in the place: <list>
- Not converted: <list with reasons>
## What you need to do
<the numbered list from Step 7>
Details: MIGRATION_NOTES.mdUpdating the kernel in a game (agents)
Only when the user asks for a kernel update (a new @typetorch/kernel version). Kernel updates stay manual: the kernel lives in the place, so it changes with a place publish and new servers. This is the one flow where you run kernel deploy, and you publish only after the user's OK in chat.
- You run a dry run:
bun run typetorch kernel deploy --dry-run. It patches only the kernel folders and the kernel's settings into the place's newest version (which must be published), checks that everything else is unchanged, and saves nothing. Read the summary (kernel old -> new, scripts changed per folder, settings, references); anything outside the kernel changing is a stop. - Show the user the summary and ask. After their yes:
bun run typetorch kernel deploy --yes. It saves and publishes through Luau Execution, with no download. It needs the place setting "Allow place to be updated using Save Place API" on, and no Team Create session. It refuses if someone published meanwhile. Undo:bun run typetorch kernel restore --version <the version before>. - If the place doesn't allow saving through the API (a Studio-made place with that setting off), the user downloads a copy in Studio (File > Download a Copy, a binary
.rbxl) and tells you the file and the place version it came from. Runbun run typetorch kernel deploy --dry-run --place-file <file> --base <version>, and after the user's yes the same command without--dry-runplus--yes. Keep the copy:bun run typetorch kernel restore <file>publishes it back (undo). - Or, with the Roblox Studio MCP and the place open: replace the kernel instances in Studio (
ServerScriptService.TypeTorchKernel,ReplicatedStorage.TypeTorchKernelShared,ReplicatedFirst.TypeTorchKernelClient, asnode_modules/@typetorch/kernel/place.project.jsonlays them out, with the sources fromnode_modules/@typetorch/kernel/src; keep the folders' attributes such asKeyAssetId), then the user publishes from Studio (File > Publish to Roblox). - Check a fresh server boots the new kernel: the user joins a new server (old ones keep the old kernel until they close); F9 > Server shows
[TypeTorch] kernel <new version>and/tt statusanswers.
Fresh setup path
No game yet ("set up typetorch" in an empty folder). The user's own path is bunx @typetorch/cli init (it asks for keys and publishes, so it is never yours to run: name it in the final list). Without it:
From the workspace folder:
shgit clone https://github.com/typetorch/template my-game cd my-game git remote remove origin bun install rokit installbun installgets every@typetorch/*package from npm, at the template's (old) pins; the template already has thetypetorchscript. Then take the newest versions and darklua (3.2, 3.3):shbun add @typetorch/framework@latest @typetorch/kernel@latest bun add -d @typetorch/transformer@latest @typetorch/cli@latest @typetorch/dev-server@latest rokit add seaofvoices/darkluaThe template's code imports its macros from
rbxts-transform-debugand its tsconfig runs that plugin before@typetorch/transformer; transformer 0.3 accepts both. Leave it, or move it as in 4.9.In
typetorch.json: the user's ids (or placeholders1),members{}unless given, and deletesigningPublicKeys,keyAssetIdandfallbackPublicKey(they are the template's).Step 5 checks, commit.
Step 7 list (skip data, legacy and kernel-copy items: the new place takes
--replace-place).
Optional: Claude Code
- Install the skill:
/plugin marketplace add typetorch/claude-plugin, then/plugin install typetorch@typetorch. Run it with/typetorch:typetorch-migrate.typetorch initadds the marketplace itself when it starts Claude Code. - The CLI is also an MCP server for the Studio loop:
claude mcp add typetorch -- bunx typetorch mcpin the game repo (tools:serve_status,serve_start,serve_stop,serve_logs,studio_logs,studio_reconnect,studio_play,studio_stop). Same limits as Step 5 item 7. - Plan mode fits Steps 1 and 2. Allow
bun run buildandbun run typetorch build; keep every Roblox-facing command on ask or deny. - remote-claude runs only on a Claude subscription login, never an API key.