Appearance
Synced from the cli README.
@typetorch/cli
typetorch builds a roblox-ts game into a payload, uploads it to Roblox as a private Model asset, and hot-swaps it into live servers without a restart. It also promotes and rolls back already uploaded builds, lists deployments with their git identity, checks and publishes the kernel place, and syncs hot assets (models and UI templates that builders edit in the place) into the artifact. Every deploy is approved by a person (typetorch approve); agents and the remote-claude dev-server only prepare them. Prod-channel deploys are signed with two Ed25519 keys, so live prod servers only run what you published. A cloud test gates prod deploys: the uploaded payload boots headless in the place before anything is published. typetorch servers and typetorch report show the live fleet and what each server did with a deploy.
Part of TypeTorch: the kernel (@typetorch/kernel) is baked into the place and swaps payloads; the framework (@typetorch/framework) ships inside every payload.
Install
The CLI runs on Node 20+ (npm, npx) or Bun 1.3+; you don't need both to run it.
sh
npm i -g @typetorch/cli # then: typetorch <command> (or the short alias: tt <command>)
npx @typetorch/cli <command> # no install
bun add -d @typetorch/cli # in the game repo, then: bunx typetorch <command>
bun src/index.ts <command> # from a checkout of this repoThe package installs two names for the same binary, typetorch and the short tt (CLI 0.12); every typetorch below works as tt too (tt deploy, tt serve --remote).
The template already has @typetorch/cli (and @typetorch/dev-server) in its devDependencies and the script "typetorch": "typetorch": after bun install, bun run typetorch <command> runs it.
What the commands call:
- git, and Rojo 7.7.x (plus Lune for
kernel deploy) through Rokit: pinrojo-rbx/rojo@7.7.0-rc.1in the game'srokit.toml. darklua (rokit add seaofvoices/darklua) for the production pack ofbuild/deploy/upload(see "Builds"); without it payloads go up unminified, with a warning. - Bun for
typetorch build/deploy/upload: the game repo is a Bun project and the build runsbun run build(orbun run rbxtsc, orbun run goblox: see "Compiler") in it. Commands that don't build (approve,promote,rollback,deployments,keys,pin,assets status,doctor) don't need it. - zstd for hot assets (
assets sync/status, which read Roblox-serialized exports): Bun, or Node 22.15+.typetorch doctorreports the runtime, Bun and zstd.
typetorch on PATH (from a checkout)
bin/typetorch.cmd (cmd.exe and PowerShell) and bin/typetorch (bash/zsh) run this checkout with Bun (bin/tt.cmd and bin/tt are the same, shorter). Add bin/ to your user PATH once, then open a new terminal. PowerShell, for this machine's checkout:
powershell
[Environment]::SetEnvironmentVariable("Path", [Environment]::GetEnvironmentVariable("Path", "User") + ";<path-to>\typetorch-cli\bin", "User")Alternatively bun link in this folder, then bun link @typetorch/cli in a game repo (that puts it on the repo's bunx typetorch, not on PATH).
Setup
New game: bunx @typetorch/cli init in an empty folder. The guided setup asks one question at a time and does the rest: the tools, the starter template, your experience (paste its URL), the Open Cloud key (probed, written to the game repo's .env), the owner, typetorch.json, the two prod signing keys and the key asset, the kernel place, an optional backend (a VPS with Coolify or a plain systemd service with Caddy, over SSH; this PC behind a free Cloudflare quick tunnel, or one you already run), the first prod deploy and a dev branch, and AGENT_PROMPT.md for your coding agent. Each step says why it exists and which doc it replaces, and checks its result the way typetorch doctor would. Stopped half way? Run it again: .typetorch/init.json (never a secret) records what is done. typetorch init --phase <name> runs one step again; --answers <file> replays answers for scripts (a secret never comes from the file).
By hand, a game repo has typetorch.json next to default.project.json:
jsonc
{
"project": "my-game",
"universeId": 123, "placeId": 456,
"creator": { "groupId": 789 }, // or { "userId": 1 }: the experience owner (LoadAsset refuses other owners)
"defaultBranch": "prod",
"branches": { "main": "prod" }, // git branch -> TypeTorch branch (default: lowercased, "/" -> "-")
"channels": { "prod": "prod" }, // branch -> "prod" | "dev" (default: prod for defaultBranch, else dev)
"members": { "123456789": "owner" }, // userId -> owner | dev (owners also default remote-claude's --users)
"revoked": { "987654321": true }, // userIds refused dev access whatever else says (`access push`)
"devBadgeId": null,
"kernel": "node_modules/@typetorch/kernel",
"approval": "all", // "all" (default) | "prod" | "none": which deploys need `typetorch approve`
// written by `typetorch keys ...` (public keys only; commit them):
"signingPublicKeys": ["…"], // trusted main keys = the key asset's PublicKeys
"revokedKeys": [], // = the key asset's RevokedKeys
"fallbackPublicKey": "…", // baked into the place by `kernel deploy`
"keyAssetId": 123, // the key asset; `kernel deploy` stamps it
"backend": { "url": "https://backend.example.com" }, // the TypeTorch backend (`typetorch backend setup` writes it)
// optional safety thresholds (see "Health window and auto-rollback settings"):
"health": { "errors": 3, "window": 30, "rollback": true, "dev": { "rollback": false } },
"autoRollback": { "failedPct": 20 },
// optional: the place's backup build (see "Backup build"); these are the defaults
"backup": { "refresh": "auto", "healthyHours": 3 },
// optional: "goblox" (needs `bun add -d goblox`) | "rbxtsc"; unset: goblox when installed, else rbxtsc (see "Compiler")
"compiler": "goblox"
}Gitignore .env, .typetorch/, src/shared/build.ts, .payload.gen.project.json and .tsconfig.typetorch.json.
Secrets go in the game repo's .env, everything else in typetorch.json (CLI 0.9, plans/21). typetorch doctor lists every value the CLI reads and where it came from (Config).
Health window and auto-rollback settings
"health": on each server a new build is rolled back when its own scripts throwerrorserrors (1-100, default 3) withinwindowseconds of starting (5-300, default 30), or anonStartfails."rollback": falsekeeps a failing build running: the server only reportsdegraded(ahealth_degradedalert)."prod"and"dev"override any of the three for builds of that channel. Per channel, not per branch: the values ride the build, andpromotemoves a build between branches without a rebuild.- Every build stamps the result on the payload root as
HealthErrors,HealthWindowandHealthRollback(the defaults when unset). Kernel 0.3.7+ reads them when it mounts the build; older kernels ignore them (3, 30 s, on). Prod payloads are signed deploys of your own uploads, so the values are trusted like the code. "autoRollback": { "failedPct": 20 }:--waitrolls the branch back when this % (1-100) of the servers that tried the build reportfailedorrolled_back.--rollback-at <pct>overrides it for one command.buildanddeployprint the health line;deployprints the auto-rollback line;doctorshows both.- A noisy game: measure first. Deploy to a dev branch, play, and count your errors in the first 30 s (dev menu > Server > Status shows "Health window: errors / limit, time left"). Then set
errorsabove that, or fix the errors.
Keys
Keys are read from, highest priority first: the real environment; the game repo's .env (next to typetorch.json: --config's folder, else the nearest folder with typetorch.json at or above the working directory). No other .env is read (before CLI 0.9 every parent folder's .env counted). --env-file <path> or TYPETORCH_ENV_FILE (real environment) is an override: that file is read instead of .env. A TYPETORCH_ENV_FILE= line inside the game's .env (the CLI 0.8 layout) is still followed (through 0.18.1), with a warning: move the keys into .env. Values Bun auto-loads from .env* files count as that file, not as the environment. Nothing read from a file is copied into process.env, a key goes only into the client for its job, and child processes (rbxtsc, rojo, lune, bun scripts, git) get an allowlisted environment without any key. Keys are never printed.
| Variable | Used for | Scopes |
|---|---|---|
OPENCLOUD_ASSETS_KEY | payload uploads and moderation (deploy, upload); the cloud test (test --cloud, the prod gate); hot assets (assets sync, assets status); doctor's place check | asset:read (also on the place), asset:write; universe.place.luau-execution-session:read + :write for the cloud test, hot assets and doctor |
OPENCLOUD_DEPLOY_KEY | deploy messages and the shared seq (deploy, rollback, promote, keys resign); the signed settings record (settings, access push, backend setup, keys rotate / resign) | universe-messaging-service:publish; universe-datastores.objects:read (+ :create and :update to claim seqs, store the durable head and write the settings; see "The shared seq", "The durable head" and "Settings"). No universe:write / universe:read: nothing is kept in ConfigService since CLI 0.8 / kernel 0.3.8 |
OPENCLOUD_PLACE_KEY | kernel deploy, kernel restore (manual only) | universe.place.luau-execution-session:read + :write (the default luau engine: Luau Execution tasks patch and save the place) and asset:read (place versions); universe.place:write only to publish files (--place-file, kernel restore <file>, --replace-place). Roblox has no API-key route for downloading place files: Asset Delivery needs legacy-asset:manage, which can't be granted to API keys, and universe.place:read shipped but only covers the place version history (/place-version-history-api/v1/{placeId}/history, /contributors; a key limited to one experience even gets 403 "Scope must be configured to allow all resources" there) |
OPENCLOUD_API_KEY (alias ROBLOX_API_KEY) | any job without its own key | all of the above |
TYPETORCH_API_KEY | the TypeTorch backend's game key (write-only): backend setup checks it and writes it into the settings record (game servers post heartbeats, events and errors with it); the CLI posts alerts and chart marks with it. Since CLI 0.9 never a Roblox key: an old value that still looks like one (the same as an Open Cloud variable, or the CLI 0.8 layout next to TYPETORCH_FLEET_INGEST_TOKEN) is refused with the rename to do | - |
TYPETORCH_ADMIN_TOKEN | the backend's admin token: servers, report, alerts, --wait, the owner list (PUT /v1/access), notify, experiment list, keys create / list / revoke; never goes into the record | - |
Old names, still read (through 0.18.1) with a warning naming the new one: TYPETORCH_FLEET_TOKEN (= TYPETORCH_ADMIN_TOKEN), TYPETORCH_FLEET_INGEST_TOKEN (= TYPETORCH_API_KEY).
Other settings: TYPETORCH_STATE_DIR (where the logs live, default .typetorch/), TYPETORCH_PROPOSED_BY, TYPETORCH_ROJO / TYPETORCH_LUNE (tool paths), TYPETORCH_BUILD_CACHE=0 (turns the build cache off), TYPETORCH_COMPILER=goblox|rbxtsc (the compiler; see "Compiler"), TYPETORCH_DARKLUA (darklua's path for the production pack; real environment only), TYPETORCH_DEBUG=1 (= --verbose), TYPETORCH_UPDATE_CHECK=off (no daily update check), TYPETORCH_CHILD_ENV=NAME,NAME (extra non-secret variables for child processes), TYPETORCH_KEY_FILE / TYPETORCH_FALLBACK_KEY_FILE (the signing key files; read from the real environment only, never from an env file, and never passed to a child process).
The signing keys are not environment variables: they are key files (see "Signing prod deploys").
Approving deploys
Every deploy, rollback and promote is approved by a person in their own terminal ("approval": "all", the default; "prod" asks only for prod-channel branches, "none" never asks). No key or passphrase is involved: it is a y/N.
- You deploy:
typetorch deployin a terminal builds, uploads and waits for moderation, then shows the details and asks y/N: one command. - Anyone else deploys (an agent, the remote-claude dev-server, a script, or you with
--propose): the same build, upload and moderation, then a proposal inproposals.jsonl(state dir) and nothing published. It printsapprove with: typetorch approve <id>. Proposals expire after 24 h.--proposed-by/TYPETORCH_PROPOSED_BYnames who prepared it (default:cliat a terminal,agentotherwise). typetorch approve [id]lists the pending proposals (newest first), shows branch, artifact, notes, sources, size, proposer and age, asks y/N, then publishes and logs it. It refuses when stdin isn't an interactive terminal.typetorch reject <id>drops one;typetorch proposals [--all]lists them.
Approval and signing are separate: whatever publishes a release to a prod-channel branch (your y/N, or "approval": "none") signs it automatically. Approval itself is enforced by the CLI only, so for dev-channel branches (unsigned) anything that holds the Open Cloud deploy key, or runs code on any server of the universe, can still publish a deploy message. Keep the deploy key away from agents.
Signing prod deploys
Releases (deploy, rollback, promote, re-sign) and pins to a prod-channel branch are signed with two Ed25519 keys when they are published; dev-channel ones never are. Every prod message, durable head and the settings record carry sig (main key) and sigF (fallback key). The kernel's rule is strict: once the key asset has loaded on a server only sig counts; while it never loaded only sigF counts (rules: Prod signing).
| Main key | Fallback key | |
|---|---|---|
| Key file (plaintext JSON, no passphrase: keep it safe and backed up) | ~/.config/typetorch/keys/<universeId>.key | ~/.config/typetorch/keys/<universeId>.fallback.key |
| Other path | --key-file or TYPETORCH_KEY_FILE | --fallback-key-file or TYPETORCH_FALLBACK_KEY_FILE |
| Where servers get the public key | the key asset (a group-owned Model with PublicKeys / RevokedKeys attributes; its id is stamped on the kernel as KeyAssetId) | baked into the place as FallbackPublicKey |
| Replace it | typetorch keys rotate (no restart; re-signs the live prod heads) | typetorch keys init --fallback --force, then typetorch kernel deploy |
Set up once, in this order:
typetorch keys init: the main key file,signingPublicKeys, and the key asset (created through Open Cloud with the assets key; its id goes intokeyAssetId). Running it again resumes or does nothing.typetorch keys init --fallback: the fallback key file andfallbackPublicKey(no network).- Commit typetorch.json (public keys only).
typetorch kernel deploy: bakesKeyAssetId,FallbackPublicKeyandBootstrapHeads(the current prod heads, which the kernel trusts unsigned: heads stored before signing have no signature) into the place's kernel (patched in; servers run it after a restart). It refuses to publish without the keys. Run it from the machine with the latest deployment log.typetorch doctor: both key files exist and match typetorch.json, the key asset (Approved, right owner, same lists) and the place's kernel attributes; any mismatch is a warning.
Recovery: a lost or leaked main key: typetorch keys rotate. The key asset trusts only the new key and revokes the old one, TypeTorch/rekey tells servers to re-read it, and then each prod-channel branch's live head is re-signed: the same artifact and asset as a fresh signed message with a new seq and r = "resign" (servers update the head, no swap), through the approval policy like any prod publish. typetorch keys resign does only that step (to retry it). A leaked or lost fallback key: typetorch keys init --fallback --force (adds the old key to the key asset's RevokedKeys first, then makes a new pair), then typetorch kernel deploy.
Signed pins (A/B experiments on prod): typetorch pin <artifact> --branch <b> (--servers <jobId,...> | --pct <1-99>) and typetorch pin --unpin --branch <b> (--servers ... | --all) publish TypeTorch/pin (kernel 0.2.3 fields) with sig/sigF on prod-channel branches, after a y/N (the approval policy; pins can't be proposals because servers drop them after 120 s). --by <userId> names the owner (default: the only "owner" in members, else the creator userId). Unsigned pins from the in-game Manage tab only work on dev-channel servers.
No command prints a seed; a key file inside the repo or any git work tree is refused. The remote-claude dev-server only deploys dev-channel branches, never gets the key variables, and is refused if it ever tries to publish to prod. Dry runs show whether signing is ready, with placeholders instead of real signatures. Anyone who can write the group's assets (including the upload/CI key) can change the key asset: an accepted risk until the keys are split (D4).
Commands
| Command | Does |
|---|---|
typetorch init [--dir <folder>] [--phase <name>] [--answers <file>] [--agent] [--teardown] | the guided setup, from an empty folder to a live game (see "Setup"); --agent prints AGENT_PROMPT.md again; --teardown removes the backend step 6 installed (asks first; its data only when you say so) |
typetorch build [--branch <b>] [--channel prod|dev] [--clean] [--no-minify] | writes src/shared/build.ts, runs the compiler (goblox when installed, else rbxtsc; bun run build if the repo has a build script), and rojo-builds .typetorch/payload.rbxm with the identity stamped on the root; checks it holds only Folders and ModuleScripts; writes .typetorch/payload.json. --clean: git clean -fdX out/ and include/ first, then restore the build cache |
typetorch upload [--branch <b>] [--channel prod|dev] [--no-build] [--no-minify] [--moderation-timeout <s>] | clean build, upload as a new Model asset, wait for moderation; no deploy (then promote it) |
typetorch deploy [--branch] [--channel] [--no-build] [--no-minify] [--dry-run] [--message <text>] [--force] [--propose] [--proposed-by <who>] [--moderation-timeout <s>] [--test] [--skip-test <reason>] [--wait [s]] [--no-wait] [--no-auto-rollback] [--rollback-at <pct>] [--rollout <1-99>] | clean build, upload, wait until Approved, log "uploaded"; the cloud test (always for prod-channel branches); then approve here (a person at a terminal) or write a proposal; on approval: deploy message, durable head, log "published"; then (prod: by default) wait for the servers' reports. Per-stage timings |
typetorch deploy --widen <1-100> [--branch] | re-send the branch's live deploy (same seq) to more servers; dev-channel branches only (see "Rollouts") |
typetorch promote <branch> <artifactId|assetId|#seq|commit> [--force] [--dry-run] [--test] [--skip-test <reason>] [--wait [s]] [--rollout <1-99>] | point a branch at an already uploaded, approved payload (from the deployments or uploads.jsonl) with a new seq; no rebuild. A prod-channel branch only takes prod-channel artifacts, even with --force ("rebuild for prod"). promote <artifact> <branch> works too when only the second is a known branch |
typetorch rollback [--branch] [--to <commit|artifactId|assetId|#seq>] [--force] [--dry-run] [--message <text>] [--propose] [--test] [--wait [s]] | point the branch at an earlier, already approved asset (no build or upload) and tell its servers; no cloud test unless --test (it is an earlier build) |
typetorch deployments [--branch] [--limit n] | deployment history with git identity; * = each branch's live head; lists uploads that never went out |
typetorch branch ls | branches (the kernel's DataStore heads and this machine's log), channels, heads, signed or not, live servers per branch; warns from 28 of the kernel's 32 stored branches |
typetorch branch rm <branch> | removes a dev branch's head from the DataStore heads (y/N or --yes; --dry-run). Refuses the default and prod-channel branches, a branch live servers run (the fleet API; without it only with --force), and a head that changed meanwhile. Servers' MemoryStore copy keeps it until it expires (up to 45 days) |
typetorch settings status / get [field|game.<key>] / set game.<key> <json|-> / unset <field> / push | the signed settings record (see "Settings"); push writes defaultBranch, channels and dev access from typetorch.json. --dry-run, --force, --no-ping |
typetorch experiment list / weights <name> <w1,w2,...> / settle <name> <variant> / stop <name> / resume <name> / clear <name> / force <name> <player> <variant> / unforce <name> <player> | live analytics experiment overrides in the signed settings record, no deploy (see "Experiments"). --variants a,b, --dry-run, --force, --no-ping |
typetorch notify push [--dry-run] / status / templates / templates set <kind> <message_id> / test <userId> <kind> [--param k=v]... [--data x] / send <userId> <template> [--param k=v]... [--data x] / key rotate [--dry-run] [--no-ping] / key rotate --backend-only / pause [<kind>] / resume [<kind>] | experience notifications on the backend, with TYPETORCH_ADMIN_TOKEN (see "Notifications"): push typetorch.json notifications, the backend's state (halted kinds stand out), the template texts to make by hand in the Creator Dashboard (and writing their ids back), a test or one-off send, a new launch-data signing key (the settings record's backend.notify.keys first, then the backend), and the runtime setting notify.enabled (or one kind's) |
typetorch access push [--dry-run] / access status | typetorch.json members, revoked, devBadgeId into the settings record's access / compare it with typetorch.json |
typetorch kernel deploy [--dry-run] [--yes] [--install] [--base published|latest|<n>] [--engine luau|splice|lune] [--place-file <file>] [--timeout <s>] [--kernel <dir>] [--no-backup] [--loadstring] | patch the kernel (and the backup build) into the live place (check, y/N, save, verify); no download: Luau Execution tasks do it (the luau engine), or --place-file patches a Studio copy (splice); --replace-place --yes for the template/test place only; see below |
typetorch kernel restore --version <n> [--dry-run] [--yes] | republish place version n (a task on it calls SavePlaceAsync): the undo of a kernel deploy |
typetorch kernel restore <file> [--dry-run] [--yes] | publish a place file: a backup from .typetorch/place-backups/, or a dry run's patched file |
typetorch backup refresh [--build <x>] [--dry-run] [--yes] [--force] | make a proven-healthy prod build the place's backup build now (luau engine, only that slot); see "Backup build" |
typetorch deploy --reupload <artifactId|#seq|commit|assetId> [--branch <b>] | moderation took down an approved build: upload its kept bytes as a NEW asset, then deploy it (cloud test, approval, new seq, signed on prod); see "Backup build" |
typetorch approve [id] [--import <dir>] [--test] [--skip-test <reason>] [--rollout <1-99>] [--wait [s]] | approve a proposal: details, y/N, publish (interactive terminal only); prod-channel ones are signed. A prod deploy or promote proposal without a passed (or skipped) cloud test runs it before the y/N. --import: a proposal state dir from automation you run yourself (see "No GitHub Actions") |
typetorch test [--cloud] [<artifact>] [--branch] [--seconds <n>] [--no-swap] | the cloud test on its own (see "Cloud test") |
typetorch servers [--branch] [--watch] | live servers from the fleet API (see "Fleet") |
typetorch alerts [--follow] [--level] [--since <min>] | the fleet's alerts (see "Fleet") |
typetorch backend setup [--url <url>] [--flush-seconds <s>] [--record-share <0-1>] [--dry-run] [--force] [--no-ping] | point game servers at the TypeTorch backend: checks, then the record's backend section (and the old fleet/analytics from it), ping, owner list, typetorch.json backend.url (see "Fleet") |
typetorch backend run [--backend-dir <checkout>] [--port <n>] [--cloudflared <path>] | the backend on this PC behind a Cloudflare quick tunnel (no account): starts the backend checkout's server (default port 8787) with the two backend keys, opens the tunnel and runs backend setup --url <tunnel> each time the tunnel's address changes; restarts either when it drops. init sets it up (.typetorch/backend.json) and registers it as a login task; the current address is in .typetorch/backend-run.json |
typetorch backend bless [--url <url>] [--no-open] | trusts one browser for admin when you sign in with typetorch.dev: sends the public signing keys to the backend, signs its one-time challenge with the main prod key (it never leaves this PC) and opens the single-use link (5 minutes) |
typetorch report <seq|artifact|latest> [--branch] | what the servers did with one deploy; exits 1 when one failed or rolled back (see "Fleet") |
typetorch keys init [--key-file] / keys init --fallback [--force] [--yes] / keys rotate [--yes] / keys resign | the signing keys: see "Signing prod deploys" |
typetorch keys create --name <name> --role <game|lookup|support|analyst|admin> [--expires 365d|90d|1y] / keys list [--all] / keys revoke <id> | (the next release after 0.18.1) named API keys with roles on the backend (plans/31), with TYPETORCH_ADMIN_TOKEN: for tools that must not hold the admin token (a support bot reading one player's rows). create prints the key ttk_<id>_<secret> ONCE (--json: in key) and never writes it anywhere; list shows id, name, role, scopes, expiry, last use, built-in / revoked; revoke ends one at once (built-in env:* keys, the env tokens, are rotated in the backend's environment instead) |
typetorch pin <artifact> --branch <b> (--servers <ids> | --pct <1-99>) / pin --unpin --branch <b> (--servers <ids> | --all) | A/B experiment pins, signed on prod-channel branches; --by, --dry-run |
typetorch reject <id> [--reason] / typetorch proposals [--all] | drop a proposal / list them |
typetorch assets sync [--dry-run] [--deploy <branch>] [--place-version <n> | --studio-export <file>] | hot assets: export the instances marked TypeTorchAsset from the place's latest published version (or the Studio plugin's export), upload new and changed ones, write typetorch.assets.lock.json; see "Hot assets" |
typetorch assets status / typetorch assets list | export + diff without uploading / the lockfile |
typetorch remote-claude [--users <ids>] [...] | runs typetorch-dev-server remote-claude (@typetorch/dev-server) with the same arguments in this terminal, and exits with its code: devs prompt Claude Code on this machine from the in-game DEV > Claude tab (dev-channel branches only). --users is remembered once given; until then the dev-server (0.3+) defaults to the owners in typetorch.json members. Found in TYPETORCH_DEV_SERVER (an entry), node_modules/@typetorch/dev-server in the game repo or a parent, next to this CLI (npm i -g @typetorch/dev-server; with npx: npx -p @typetorch/cli -p @typetorch/dev-server typetorch remote-claude ...), or a sibling ../dev-server checkout; else a clear error. It gets the real environment (the dev-server reads the key, --env-file / TYPETORCH_ENV_FILE and .env itself) |
typetorch dev [--users <ids>] [...] | the same as typetorch remote-claude |
typetorch serve [--detach] [--project <file>] [--port <n>] [--no-watch] [--no-live] [--with <scripts>] [--dev-branch <b>] | the Studio dev loop in one process: rbxtsc -w, rojo serve, the game's live script (on by default when package.json has one; --no-live off), extra package.json scripts, and the control API the TypeTorch Studio plugin attaches to by itself; --dev-branch: where the plugin's "Publish to dev" deploys hot assets (default dev); see "Studio: typetorch serve" |
typetorch serve --remote [--remote-port <n>] [--url <your tunnel> | --no-tunnel] | also lets a Studio on another machine in: a gateway on 127.0.0.1:34890 behind an automatic Cloudflare quick tunnel (cloudflared installed if missing), and one pairing link to send; see "A Studio on another machine" |
typetorch serve --claude [--claude-users <ids>] | also runs remote-claude for the plugin's Claude tab; --claude-users defaults to the owners in typetorch.json (remembered once given); see "Claude in Studio" |
typetorch serve pair [--url <tunnel URL>] / devices / unpair (--name <device> | --all) | another one-time pairing link (15 min) / the paired devices / revoke them |
typetorch serve status / stop / restart <watch|rojo|claude|<script>> / logs [--studio] [--since <seq>] [--limit <n>] [--follow] | the running server's children, attached Studios, sync state, compile errors and pairing link / stop it / restart one child / its log |
typetorch studio status / reconnect [--anyway] [--target <id|edit>] / play [--run] / stop / logs [--since <seq>] [--limit <n>] [--follow] | drives the Studio the plugin attached: its sync state / resync from scratch / start / end a playtest / Studio's Output. --timeout <s>: how long to wait for Studio (default 20) |
typetorch studio install-plugin [--from <dir>] | builds the TypeTorch Studio plugin (the game's devDependency @typetorch/studio-plugin, or --from a checkout) with Rojo into Studio's plugins folder as TypeTorchSync.rbxm |
typetorch assets types [--out <file>] | writes src/shared/assets.gen.ts: the hot asset keys with their classes and a HotAssetKey type (sync refreshes it once it exists) |
typetorch mcp | an MCP server on stdio with serve and studio as tools, for agents |
typetorch migrate --from flamework [--dry-run] [--net compat|native] [--report <file>] [--allow-dirty] | the mechanical part of moving a Flamework 1.x game (see "Migrating from Flamework"); local only, no keys |
typetorch update [<version>] [--check] [--yes] | updates this CLI to the newest @typetorch/cli on npm (or <version>) the way it was installed: in the game's package.json with its package manager (bun, pnpm, yarn or npm, from the lockfile; a devDependency stays one), or globally (npm i -g, bun add -g, pnpm, yarn). Asks y/N first (--yes skips; without a terminal it prints the command). npx needs nothing (npx @typetorch/cli@latest); a git checkout gets the git pull to run. --check only shows the versions and the command. After any other command at a terminal, the CLI checks npm at most once a day (1.5 s timeout, cached in ~/.typetorch/update-check.json) and, when a newer version is out, asks once whether to update now; after a no it reminds once a day. Never in CI, --json or non-interactive runs; TYPETORCH_UPDATE_CHECK=off turns it off |
typetorch doctor [--show-ok] | prints the Config section (every value the CLI reads and where it came from: typetorch.json, the game repo's .env, the environment, the settings record; secrets only as set / unset), then checks the runtime, bun, zstd, git, rojo 7.7.x, the compiler (goblox or rbxtsc and why; a warning when goblox is selected and the tsconfig lists a JS plugin other than @typetorch/transformer, which goblox ignores; with rbxtsc it suggests bun add -d goblox), roblox-ts, typetorch.json, the env file, each job's key, the approval policy, the state dir, the health window per channel and the auto-rollback threshold, the signing keys (key files vs typetorch.json, the key asset, the place; the place and the key asset's content through one Luau Execution task with the assets key; the same task warns when the place still holds ServerStorage.TypeTorchDev, the Studio local payload folder that live servers ignore), the backend (typetorch.json backend.url with your keys: url, /healthz, the game key's and the admin token's roles; the record's own backend section; a stale tunnel or a wrong key is a FAIL with the fix), the backend's owner list against the signed record, live servers (FAIL when the published place has the kernel, the default branch has no verified head in the DataStore heads or the kernel's BootstrapHeads, and the place has no backup build: kernel 0.3.6+ then moves every player out after 15 s and kicks after 3 bounces; the fix names the git branch mapped to the default branch and kernel restore --version <n>), old game build (warn when a roblox-ts build such as ServerScriptService.TS or ReplicatedStorage.rbxts_include runs next to the kernel), and probes each key's scopes with harmless calls. Only problems and info lines print; --show-ok lists the passed checks too (--json always has every check) |
Every command takes --json (one JSON document on stdout; human lines go to stderr), --verbose, --config <path> and --env-file <path> (remote-claude passes everything to the dev-server). Under Node, an --env-file naming a missing file is reported by Node itself (node: <file>: not found, exit 9): Node checks that flag even after the script name.
Long waits name what they wait on. On a terminal, one line on stderr is redrawn while anything takes longer than a quarter second, e.g. ⠹ 12s waiting on: moderation of asset 123 (12s), publish message (1s) (jobs over 10 s turn yellow; |/-\ instead of braille on Windows consoles without UTF-8). Log lines clear it first, prompts hide it, and the cursor comes back on exit and on Ctrl+C. Without a terminal (CI, agents, pipes) there is no animation: a job past 15 s prints still waiting on: ... every 15 s. --json prints none of it.
Migrating from Flamework
typetorch migrate --from flamework, in the game folder (next to tsconfig.json), rewrites the source with the TypeScript compiler API, using the game's own node_modules/typescript (no extra dependency). It refuses a working tree with uncommitted changes (--allow-dirty), so the migration is one diff to review; --dry-run prints that diff and writes nothing.
- Rewrites:
@flamework/coreimports to@typetorch/framework(unsupported names are dropped when unused, else kept and flagged);@Service/@Controllerclassesextends Module(or their local base class does) and callsuper(); a module's owntrove = new Trove()gives way to Module's;@metadata flamework:*tags; ignite files (*.server.ts/*.client.tswithFlamework.ignite()) becomesrc/<realm>/boot.tswith theaddPathsfolders, and their other top-level code moves into a generated module'sonStartin the first folder. - Networking:
--net compat(default) swapsNetworking.createEvent/createFunctionfor the framework'screateFlameworkCompat, so every call site stays as it is.--net nativemerges each file's networks into onecreateNetworkand rewrites the call sites (connect->oninthis.trove,setCallback->handle,broadcast->fireAll,except->fireExcept,predict->emit,fire(players[])->fireList, the call shorthand ->fire/invoke), deletes handler files that only created handlers, and flags what it can't decide. - Flags (not rewritten): module-level state,
Players.PlayerAdded.Connect,_G, loops /task.*/ connections outside a trove,@flamework/components,Dependency<T>()before construction,loadstring, remotes, MessagingService, DataStore / ProfileService code, BindToClose, Scripts left in the source tree, toolchain leftovers. - Output:
typetorch-migrate-report.md(--report): a summary, every flag by kind with file, line and the fix, and what was rewritten; plus a short summary on the terminal (--jsonfor both as data). Both suggest the optional fast compiler,bun add -d goblox(see "Compiler"; it ignores JS tsconfig plugins other than@typetorch/transformer).
typetorch build warns while the game still uses createFlameworkCompat (its calls are part of the protocol hash).
Identity
- Artifact id:
<commit7>-<hash6>, e.g.12b63b9-3fa91c, or<commit7>-dirty-<hash6>for a working tree with uncommitted changes (uncommitted-dirty-<hash6>without commits).commit7is the first 7 hex of HEAD (what$git("Commit")compiles in);hash6is the first 6 hex of the SHA-256 of the payload built with the id minus its hash. Same bytes, same id (a promote keeps it); different bytes, different id (every rebuild stamps a new build time). The channel is metadata, not part of the id. Legacy ids (dev-12b63b9,prod-12b63b9.r2,asset-<id>) are still read everywhere, includingrollback --toandpromote. - Deploy number:
#seq, global and monotonic: the handle to paste when something goes wrong. - Branch:
--branch, elsetypetorch.jsonbranches[gitBranch], else the git branch lowercased with/→-. - Channel:
--channel, elsechannels[branch], elseprodfordefaultBranch, elsedev. A prod-channel branch refuses a dev-channel or dirty artifact unless--force. - Sources: the game commit (
template), and for the framework and kernelv<version>of the installed npm package, or, with the template's optional local override (bun run packages, which packs sibling checkouts and writes.typetorch/packages/manifest.json), the checkouts' commits.<commit>*means a dirty checkout. Stamped as payload attributes and asSOURCESin build.ts, and logged. - Payload root attributes:
ArtifactId,KernelApi(1),Channel,Commit,BuiltAt(unix seconds),SourceTemplate,SourceFramework,SourceKernel,Notes,ProtocolHash, andAssets(also on the payload'sServerfolder; see "Hot assets"). ProtocolHash(p1-<16 hex>; kernel 0.3.2 lets client events cross a hot swap when the old and new payloads carry the same one): a hash of the arguments of everycreateNetwork(...)call in the compiled game (out/), which @typetorch/transformer generates from the network interfaces (every leaf's path and argument guard, both directions), canonicalized so comments, whitespace, file paths and the order of unions, literal lists and fields don't count, plus the framework's net runtime (out/net/runtime.luau: a new wire format is a new protocol). NocreateNetworkcall, nothing stamped.typetorch buildanddeployprintprotocol unchanged since #Norprotocol changed since #Nagainst the branch's previous deploy (recorded asprotocolHashin the logs).Notes(what the dev menu shows): a JSON string, at most 4000 bytes,{"v":1,"message":"…","changes":["template: …","framework: …"],"sources":{"template":"…","framework":"…","kernel":"…"},"built":"<ISO>","branch":"…"}.messageis the deploy's--message(remote-claude passes Claude's summary);changesare the game's commits since the branch's previous deploy, then framework and kernel commits (first deploy of <branch>orrebuild, no source changeswhen there are none). Change lines are dropped first when it is too long.- Asset name:
tt-<branch>-<artifactId>[-<channel>](only[a-z0-9-], at most 50 characters; the channel only when--channeloverrides the branch's). Roblox's text filter censors some names to####, so after the deploy message the CLI renames a censored asset toTypeTorch payload. - Asset description: only
artifact=<id>andcommit=<sha7>. Roblox's text filter censors descriptions too (a longer one came back as all#), so the notes live in theNotesattribute. After the upload the CLI reads the description back and warns when it was censored (harmless). src/shared/build.tsends with a// <build time>line so its text changes on every build (rbxtsc's incremental compile skips unchanged files). Repo build scripts that write build.ts themselves should skip it whenTYPETORCH_SKIP_BUILD_INFO=1.
Compiler
typetorch build (and deploy / upload) compiles with goblox when it is installed in the game repo, else with rbxtsc (roblox-ts), as before. goblox is roblox-ts ported to Go on typescript-go (TypeScript 7): the same CLI flags, out/ layout and tsbuildinfo-based incremental builds, several times faster. Optional:
sh
bun add -d goblox(@typetorch/goblox is an alias package for the same goblox bin.) Which compiler runs, first match wins:
TYPETORCH_COMPILER=goblox|rbxtsc(environment or.env), then"compiler": "goblox" | "rbxtsc"in typetorch.json;- goblox when
node_modules/.bin/goblox(goblox.exe/goblox.cmdon Windows) exists; - rbxtsc.
goblox asked for explicitly but not installed fails the build (it never falls back silently); any other value is an error. --verbose shows the compiler and why it was picked, and the timing line names it (goblox 1.9 s). goblox runs @typetorch/transformer natively (a Go port, from the same tsconfig plugins entry, debug-macro stripping included) and ignores every other JS tsconfig plugin (rbxts-transform-debug too): typetorch doctor warns when goblox is selected and the tsconfig lists one. Set "compiler": "rbxtsc" to keep such a plugin.
Builds
- Clean deploys:
deployanduploadrungit clean -fdXon out/ and include/ (ignored files only) before building. A clean id is refused when git-ignored files sit in the payload's source dirs, or a compiled file has no tracked source. - Build cache: after a successful clean build, out/ (with rbxtsc's
tsconfig.tsbuildinfo) is copied to.typetorch/build-cache/<channel>/with a sha256 per file. The next clean build cleans out/ as above, puts the cache back, and rbxtsc recompiles only what changed (a one-file edit: seconds instead of a full compile). The cache is dropped when any file fails its hash or when tsconfig.json, package.json or a lockfile changed; outputs whose source is gone are removed first; dev and prod never share one. Switching to goblox, or to another goblox version, is a full compile (the compiler is in the cache key; rbxtsc's keys are what they were before goblox). Needs"incremental": truewith the tsbuildinfo inside outDir.TYPETORCH_BUILD_CACHE=0turns it off. - Production pack (
src/pack.ts):build,uploadanddeploypack a transformed copy of the project's$pathfolders (.typetorch/pack/), so the artifact id, the signature and thetypetorch testgate all cover the final bytes: comments and whitespace stripped by darklua's parser (lines stay where they are, so error traces and analytics still match),__DEV__set to false with its dead branches removed (skipped with a warning when game code sets it), and*.storymodules no other module names left out. Packages are never shaken (their folder names aren't the names code requires them by). Needs darklua (rokit add seaofvoices/darklua;TYPETORCH_DARKLUApicks a binary): without it the build warns and packs unminified. Each module's output is cached by content hash in.typetorch/build-cache/pack/, and a darklua failure fails the build.--no-minify(build, upload, deploy) turns it off. The build also reads"minify": falsefrom typetorch.json (and the darklua warning suggests it), but the config check doesn't know the key yet: it warnsunknown key "minify" (ignored)and drops it, so only--no-minifyworks.typetorch servenever packs. On a 1160-module game: about 2-3 s cold, under 0.5 s unchanged, the rbxm 26-27% smaller. - Payload check: the built
.rbxmmust be oneModelholding onlyFolders andModuleScripts; anything else (Scripts, values from.txtfiles, models from.rbxmfiles...) fails the build with its path. - Debug macros per channel: dev builds keep the
$print/$warn/$dbgmacros with their[src/file.ts:line]prefixes. Prod builds drop them:$print/$warnare removed (arguments not evaluated),$dbg(x)becomesx,$assert/$errorkeep their check without the source path. Every compile getsTYPETORCH_CHANNELandTYPETORCH_DEBUG_MACROS(1/0);@typetorch/transformer0.3+ declares the macros itself (@typetorch/transformer/macros) and strips them when the variable is0, so such a project needs norbxts-transform-debug. The generatedsrc/shared/build.tsimports from whichever of the two is installed (the transformer's when it shipsmacros.d.ts). Prod builds compile with a generated.tsconfig.typetorch.json(deleted afterwards): oncerbxts-transform-debugis uninstalled it only addsdebugMacros: falseto the transformer's plugin entry (a different plugin list, so rbxtsc's incremental state never reuses dev outputs for prod); while it is still installed (rbxtsc only: goblox runs no JS transformer), the first plugin is a CLI transformer doing the same stripping. The CLI appends-p .tsconfig.typetorch.jsonto a build script that ends with the compiler (rbxtscorgoblox), else it runs the compiler directly; a build script that ends with the other compiler is skipped the same way (with a warning).
Deploy message
POST /cloud/v2/universes/{universeId}:publishMessage, topic TypeTorch/deploy, message {"b":branch,"a":assetId,"i":artifactId,"s":seq,"c":commit,"ch":channel,"t":unixMs,"r":1?,"ro":pct?,"sig":"…","sigF":"…"} (ro: the rollout %, dev-channel only, see "Rollouts"; r: 1 for rollbacks, "resign" for heads re-signed by keys rotate; sig/sigF only for prod-channel branches: base64 Ed25519 over tt1\n<b>\n<a>\n<i>\n<s>\n<c>\n<ch>\n<t>\n<r>, rules in Prod signing). Servers on branch b swap, and persist it as their branch head (the higher s wins; heads are ordered by (seq, time)). TypeTorch/rekey {"t":unixMs} tells servers to re-read the key asset (after keys rotate).
Logs and the state dir
The state dir (TYPETORCH_STATE_DIR, default .typetorch/) holds deployments.jsonl ("published" lines, with proposalId and proposedBy when approved from a proposal), uploads.jsonl ("uploaded" lines, written as soon as moderation answers, before anything is published), proposals.jsonl (proposed / approved / rejected / failed events) kernel-deploys.jsonl, assets.jsonl (hot assets: created / uploaded / failed / synced), tests.jsonl (every cloud test: asset, pass or fail, seconds, place version, task), rollouts.jsonl (deploy --widen) and experiments.jsonl (every typetorch experiment change: who, when, the settings seq, the override before and after). A deployment line carries test ({ok, seconds, placeVersion} or {skipped: "<reason>"}) and rollout when they apply. Choosing a seq and logging it happens under deploy.lock there (assets sync holds assets-sync.lock instead), and logs are only appended. The remote-claude dev-server points the deploys it runs from its worktree at the main repo's state dir, so both share one log and one seq. If a deploy stops after the upload, typetorch deployments lists the upload with its typetorch promote command.
Settings
Kernel 0.3.8 reads ONE signed record instead of ConfigService (CLI 0.8 removed the ConfigService registry, config push and every universe:write / universe:read use): the game's DataStore TypeTorch, key settings, { v: 1, seq, at, body, sig, sigF }. body is JSON text with defaultBranch, channels, access (members, revoked, devBadgeId), backend ({url, key, analytics?, notify?}; CLI 0.9; analytics = {flushSeconds?, recordShare?, experiments?}, plus dials no CLI command writes yet that the framework reads (techEvery, recordSeconds, positions, joinData, joinLaunchPrefix) and backend setup keeps; notify = {keys: {current, previous?, since}}, the launch-data HMAC keys from typetorch notify key rotate, masked wherever shown and kept by backend setup), the old fleet ({url, token}) and analytics (the sink settings) derived from it for kernels before 0.4 and game (the game's live values, TypeTorch.liveConfig); at most 32 KB, game at most 16 KB. sig and sigF are Ed25519 by the main and the fallback prod key over the lines tt1settings, <seq>, <at> and <body> joined with \n: the body is the exact text stored, so nothing is re-encoded before verifying.
Every write (settings set|unset|push, access push, backend setup, keys rotate|resign) reads the record, refuses one your keys didn't sign (--force replaces it and drops its fields: they're never re-signed), changes one field, raises seq, signs with both keys and writes it with matchVersion (or exclusiveCreate), starting over when another machine wrote in between. Then a ping on TypeTorch/deploy, {"k":"settings","s":<seq>}, makes 0.3.8 servers read it within seconds (--no-ping: within about a minute). All of them need both key files (else "run typetorch keys init"). settings status and get print no token. After every write the record's owners go to the backend (PUT /v1/access {seq, owners} with TYPETORCH_ADMIN_TOKEN; only owners may Sign in with Roblox there): a backend that is down, refuses, or holds a newer seq is a warning, never a failed write; typetorch access push sends them again. Servers verify by the same strict rule as prod deploys and refuse an older seq, so game code (which can write DataStores) can't change it; it can only put back an older signed copy, which running servers refuse (a fresh write fixes it).
Endpoint checks (the backend never reaches the record broken). A wrong address or key in the record is not rejected by anything else: game servers just fail every request ("NetFail", HTTP 401 or 530) until someone looks at the dev menu. So typetorch backend setup tests the value BEFORE reading the record or signing, and typetorch doctor runs the same tests against the live record:
| Step | What is checked | Typical fix it prints |
|---|---|---|
url | parses; https (the kernel's Fleet module and Roblox servers only use https); no user info; the base address, not an endpoint path, and only the characters the kernel accepts | give the public https address |
healthz | GET <base>/healthz answers 200 {"ok":true} within 5 s; redirects are reported, not followed | a dead quick tunnel: start bun run local again; a stopped server; a wrong host |
key | GET <base>/v1/auth/check (no side effects) with TYPETORCH_API_KEY says role game (write-only) and that the fleet and analytics parts run | put the backend's API key in the game repo's .env |
admin | the same with TYPETORCH_ADMIN_TOKEN says role admin | put the backend's admin token in .env |
A failure prints each failing step with a fix: line in red, writes nothing (no record, no ping, no typetorch.json change) and exits 1; --force writes anyway and prints the failures as warnings, except when the value in the key's place is the admin token or anything else that isn't the game key (the same value for both, or role admin): that is refused with --force too, because the record is readable by every server script (security review M2). --dry-run runs the checks too. Server text in messages has terminal escapes and control characters removed, and a redirect's Location is scrubbed of the key (L5). doctor lists backend url / healthz / key / admin for typetorch.json's backend.url with your keys, the record's own backend (or the old fleet / analytics sections of a CLI 0.8 record), and compares the backend's owner list (GET /v1/access) with the signed record's owners.
Experiments
The framework's analytics experiments (analytics.experiment("tutorial_ui", ["old", "new"]) in game code) split players by a hash of the experiment name and the player id. typetorch experiment changes that split live, through the same signed write as settings set (read, verify with your keys, change, sign with both keys, write with matchVersion, ping), so servers pick it up within seconds and no deploy is needed:
sh
typetorch experiment list # every override and its state, with the variants the backend saw
typetorch experiment weights tutorial_ui 3,1 # 75% get the first variant, 25% the second
typetorch experiment settle tutorial_ui new # everyone gets "new" (stamped, forced = true): make it final
typetorch experiment stop tutorial_ui # everyone gets the control (the first variant), not stamped
typetorch experiment resume tutorial_ui # drop stop / settle, keep the weights
typetorch experiment clear tutorial_ui # remove the override: equal split again
typetorch experiment force tutorial_ui 123456 new # one player (UserId or pid) gets "new", over everything else
typetorch experiment unforce tutorial_ui 123456 # back to whatever the override says for themforce writes players = { [UserId or pid]: variant } into the override (at most 200 players per experiment): that player gets the variant over stop, settle and the weights, stamped with forced, for QA and support. The player is a Roblox UserId or an analytics pid (32 hex, the pid column). Each client's hello carries only its own entry, never the other players' ids. It needs a framework that reads players; older frameworks ignore it. resume keeps forced players, clear drops them.
The overrides are backend.analytics.experiments = { [name]: { active?, weights?, variant?, players? } } (what framework 0.4+ reads), mirrored into the old analytics.experiments for frameworks before 0.4; backend setup keeps them (and moves overrides that sat only in the old section into backend.analytics). Names are 1-64 letters, digits and _ - . /; weights are 1-16 numbers >= 0 that don't all add up to 0; a variant is 1-32 characters. The record needs a backend (typetorch backend setup) first. Weights move as few players as possible: a player only switches when the boundary moves past their hash, and resume puts everyone back where the weights say.
The variants themselves live in game code, so the CLI can't check them: weights go in the order the game lists the variants (another count is ignored by servers: equal split), and settle takes the variant exactly as the game names it (a name the game doesn't use is ignored). experiment list shows the variants the backend's experiment rows named in the last 7 days (--days), through POST /v1/sql with TYPETORCH_ADMIN_TOKEN and typetorch.json backend.url; --variants a,b (the game's list, in order) makes weights and settle check the count and the name before anything is written. --dry-run shows the override before and after and writes nothing. settings status lists the overrides and settings get backend shows them (the key stays hidden). Every change is logged to experiments.jsonl in the state dir.
--no-registry (deploy, rollback, promote, approve, kernel deploy) is accepted and prints a one-line note. --require-shared-seq and --require-registry are gone (CLI 0.9).
Notifications
Experience notifications (plans/29; the guide: docs guides/notifications.md). typetorch.json declares the kinds:
jsonc
"notifications": {
"dailyCap": 1, "quietAfterLeaveMinutes": 120, "lapse": [[14, 7], [30, 2], [60, 1]],
"unopenedBackoff": { "after": 5, "factor": 0.5, "days": 14 },
"kinds": {
"gift_new": { "text": "{sender} sent you {gift}!", "template": "<message_id>", "priority": 10, "repeat": "per_key", "send": "preferred_hour", "maxWaitHours": 48 },
"streak": { "template": "<message_id>", "priority": 60, "repeat": "daily" }
}
}The CLI checks it with the backend's rules (kind names [a-z0-9_]{1,24}, not custom; priority 0-1000; perWeek 1-7; lapse in ascending days; dailyCap 1-3; template at most 128 characters; text at most 99 characters with balanced {placeholders} named [a-zA-Z0-9_-]{1,32}; unknown fields refused) whenever it loads typetorch.json, and typetorch notify push sends it as-is (PUT /v1/notify/config). No API creates Roblox's notification strings, so typetorch notify templates prints each kind's text to paste in the Creator Dashboard (the experience, then Engagement > Notifications) and notify templates set <kind> <message_id> writes the new id into typetorch.json (only that value changes); notify push warns about kinds with a text but no id. --param names may hold dashes ({user-streak-remaining}). notify status shows what the backend has, including a kind it halted because Roblox answered 404 for its template (fix the template id, notify push, then notify resume <kind>) and all sending halted because Roblox refused the backend's Open Cloud key (halted: "auth"). notify pause / resume set the backend's runtime setting notify.enabled (with a kind: notify.kinds.<kind>.enabled) through PATCH /v1/admin/settings. notify key rotate makes a new launch-data key (32 random bytes, base64url), signs it into the settings record's backend.notify.keys first (game servers are the only verifiers, so they learn it before anything is signed with it), pings the servers, and then sends it to the backend (PUT /v1/notify/keys {current, previous, since}), which signs with it from then on. The old key stays valid as previous for 14 days (a tap's lifetime). If the backend step fails, the backend keeps signing with the old key (still accepted as previous): notify key rotate --backend-only re-sends the record's keys. With --no-ping the backend step is left for --backend-only once servers have read the record (within about a minute). Keys show only as fingerprints (the first 8 hex of their SHA-256). typetorch doctor probes the Open Cloud key (OPENCLOUD_NOTIFY_KEY, else the shared key) for user.user-notification:write with an empty request that can't send anything.
The shared seq
Every machine (your PC, CI, the remote-claude dev-server) must take deploy numbers from one sequence: servers order heads by seq and ignore one at or below the seq they applied. The next seq is the highest of:
- the state dir's
deployments.jsonl(this machine); - the game's DataStore
TypeTorch(Open Cloud DataStores, the deploy key withuniverse-datastores.objects:read): the kernel'sheadsanddeploymentsrecords (every live server records each deploy message it hears, so they lag only while no server runs) andseq, the CLI's own counter;
plus one. When the key also has universe-datastores.objects:create and :update, the seq is claimed with one atomic increment of seq (by enough to clear every source), so two machines deploying at once never share a number, whether servers run or not. The deployment line records where it came from (seqSource: counter, read or local). Without the read scope a deploy warns and uses this machine's log only.
The durable head
After the deploy message goes out (deploy, promote, rollback, approve, the automatic rollback, and deploy --widen), the CLI also writes the head into the game's DataStore TypeTorch: heads.<branch> (the shape the kernel stores, with sig/sigF for prod) and the deploy's entry in deployments (newest first, 100 kept). So a deploy made while no server of that branch runs isn't lost: with kernel 0.3.5, new servers boot it and running ones pick it up within about a minute (the kernel reads this copy every ~60 s and writes the MemoryStore copy back). It is a read-merge-write guarded by the entry's version (DataStores v1 matchVersion, or exclusiveCreate for a new key), retried when a game server wrote the key meanwhile, and a branch's seq is never lowered. It needs the shared seq's scopes (universe-datastores.objects:read, :create, :update); without them the deploy still succeeds, with one warning: "the head isn't stored durably: deploys reach only running servers". typetorch doctor checks the write scopes (scope datastore write, a tiny value under the key doctor).
CLI 0.8.1 (kernel 0.3.9): heads.<branch> also keeps the branch's last 3 heads before it as prev (newest first, the same shape). /tt rollback on a server that booted straight into a bad build uses them (a server-local swap; prod servers take only a previous head whose signature verifies); typetorch rollback <branch> still rolls the whole branch back. Records written before have no prev (the kernel falls back to the deployment history). typetorch deployments shows each row's branch channel (prod: the default branch or one configured prod), with the build's channel in brackets when it differs.
Chart marks on the backend
CLI 0.9.1 (src/marks.ts): after a release or a place publish the CLI tells the backend, so the explorer's Deploys page and its chart markers show it. With typetorch.json backend.url and the game key (TYPETORCH_API_KEY), like alerts:
| After | Request |
|---|---|
every release: deploy, approve, promote, rollback (an automatic one too), keys rotate / resign | POST /v1/fleet/deploy { j: "cli", s, b, a, ch, t, k, fr?, m? } (k = deploy, rollback, promote or resign; fr the build the branch ran before; m the note, at most 200 characters) |
kernel deploy, kernel restore | POST /v1/fleet/mark { j: "cli", k: "kernel", v?, pv?, m, t } (v the kernel version, pv the new place version, m a one-line summary) |
backup refresh, manual or automatic | POST /v1/fleet/mark { j: "cli", k: "backup", b, s, a, ch: "prod", pv?, m, t } |
t is unix ms. Best effort: 5 s timeout, never fails or slows the command (one dim mark ... no chart mark for ... line when it didn't go through; nothing in --json runs). No backend.url or no game key: nothing is sent.
Studio: typetorch serve
typetorch serve replaces the terminals of the Studio loop (bun run watch, bun run studio, and anything else with --with <script>; the game's live script runs by default when package.json has one, --no-live turns it off) with one process, and adds a control API the TypeTorch Studio plugin (typetorch/studio-plugin: bun add -d @typetorch/studio-plugin, then typetorch studio install-plugin) attaches to by itself. The plugin also ships as TypeTorchSync.rbxm on its GitHub releases: drop that into Studio's plugins folder instead if you don't want the devDependency.
- Children.
watchruns the game'swatchscript (elsebun run rbxtsc -w);rojorunsrojo serve studio.project.json(elsedefault.project.json;--project) on Rojo's default port 34872 or the next free one, so the stock Rojo plugin still works; each--withscript runs asbun run <script>. A child that exits is restarted with backoff (0.5 s doubling to 10 s; reset after 30 s up). Ctrl+C ortypetorch serve stopstops them all (the whole process tree on Windows). Children get the CLI's minimal environment, no keys. - Compile state. The
rbxtsc -woutput is parsed:serve status(and the plugin's panel) shows "2 errors" with file, line and message, so an agent doesn't scrape a terminal. - Control API.
127.0.0.1, the first free port of 34880-34887. Every request needsX-TypeTorch: 1, a loopbackHostand noOrigin; no CORS, so a browser page can't call it..typetorch/serve.json(git-ignored with.typetorch/) records the port and pid;serve status,stop,restartandlogsfind the server through it and check its/tt/hello(same pid) before sending anything. - The plugin attaches by itself. In edit mode it probes 34880-34887 every 2 s, takes the server whose
typetorch.jsonuniverseIdequals the place'sgame.GameId, and runs Rojo's own sync (Rojo 7.7.0-rc.1's ServeSession, vendored) against that server's Rojo. A dropped connection or a restartedserveis reconnected with a full resync, backing off to 5 s. A place that doesn't match (another experience, or an unpublished file) is only synced after a click on Connect anyway. Auto-connect can be turned off per place. Its panel (the dev menu's look) shows the sync state, the server, the last sync and the compile errors. Connect only one of the stock Rojo plugin and this one at a time. - Agents.
typetorch serve --detachstarts it in the background (log:.typetorch/serve.log) and returns once it answers; every subcommand takes--json. In the foreground,--jsonprints one JSON line per log line. - Live playtest. In a playtest the plugin also runs in the server DataModel, syncs Rojo into the running game and sets
LiveReloadonServerStorage.TypeTorchDevwhile that sync is up; the kernel (0.5.4+) then remounts the local payload a moment after each change, so a save reaches the playtest without Stop + Play (server and client code, like Reload). It follows the edit session's rules (the matching experience, or a server accepted with Connect anyway; auto-connect on) and can be turned off in the panel ("Live playtest"). - Hot assets without the CLI. The panel's Hot assets card marks every selected instance with Add (a key from its name, or the name box) and unmarks with Remove, and shows each marked instance as new, changed, up to date or what a sync would refuse (bad or duplicate keys, scripts inside, nesting). The plugin exports the marked instances from the open place itself (
SerializationService), so nothing has to be published first: Check, Upload and Publish to dev runtypetorch assets status,assets syncandassets sync --deploy <dev branch>insideserveon that export (--studio-export; its env file and keys; progress inserve logs). Check needs no key. The dev branch isdev(serve --dev-branch <b>); a prod-channel branch is refused, and the deploy follows the approval policy like any other (a proposal when it needs approval). While the panel is open the check also runs by itself every 30 s (skipped while a job runs or nothing is marked; an export equal to the last check's with the lockfile unchanged never reaches serve), and the card says when it last ran. Each new, changed or removed asset gets a Diff button: the instances added, removed and changed, with old and new property values (seeassets status --jsonunder "Hot assets"). - An agent can always get Studio back. The plugin long-polls the control API for commands whatever its sync is doing (down, backing off, waiting for a click, paused), so a broken sync doesn't cut the agent off:
typetorch studio reconnectturns auto-connect on, resyncs from scratch and waits to report whether it connected (--anywayalso accepts a place of another experience or an unpublished one);studio play [--run]andstudio stopstart and end a playtest (StudioTestService; the plugin also runs in the playtest's server and client);studio logsis Studio's Output, edit and playtest, as the plugin forwards it. When no Studio is attached at all, the error says why and gives the last resort: settingServerStorage'sTypeTorchReconnectattribute (Studio's command bar, or the Roblox Studio MCP'sexecute_luau) makes the plugin reconnect:game:GetService("ServerStorage"):SetAttribute("TypeTorchReconnect", os.time()). - MCP.
typetorch mcpserves the same as tools over stdio (serve_status,serve_start,serve_stop,serve_logs,studio_logs,studio_reconnect,studio_play,studio_stop). Claude Code, from the game repo:claude mcp add typetorch -- bunx typetorch mcp. - A Studio on another machine (
serve --remote). serve also opens a gateway on127.0.0.1:34890(--remote-port) and a free Cloudflare quick tunnel to it (no account; cloudflared is installed when it's missing, as the dev-server does), so someone else's Studio can attach over the internet. Once the tunnel answers, serve prints a one-time pairing link (15 min): the tunnel URL with the code after a#(typetorch serve statusshows it too;typetorch serve pairmakes another). A quick tunnel's address changes when cloudflared restarts, and serve then prints a new link. To use a tunnel you already run (a named Cloudflare tunnel routed tohttp://localhost:34890), pass--url https://<your tunnel>;--no-tunnelopens none and leavestypetorch serve pair --url <it>to you (the URL is remembered, so later it's justtypetorch serve pair).serve --remote --detachwaits for the tunnel and prints the link (linkin--json) before it returns. The other person pastes that link into the plugin's Remote card, and their Studio stays paired for 30 days (serve devices,serve unpair --name <device>|--all). Every request but pairing needs that device's token (kept hashed in.typetorch/serve-remote.json). A remote Studio syncs code read-only through Rojo (never Rojo'swriteoropen), reports its state and Output, and runs the hot asset buttons (uploads with your keys, deploys only to the dev branch). It can't stop or restart serve, read serve's log or drive other Studios, andtypetorch studio ...and the MCP tools never pick a remote Studio unless--targetnames it. Wrong codes lock pairing after 5 tries until you make a new code. - Claude in Studio (
serve --claude). serve also runs remote-claude (typetorch remote-claude, from@typetorch/dev-server) on this machine without a tunnel, and the plugin's Claude tab prompts it: Claude Code edits a copy of this branch (Code mode; a prod-channel branch works too, onremote-claude/<branch>), then the tab shows Apply (the change goes into this checkout, uncommitted, so Rojo syncs it to Studio), Deploy to dev (--dev-branch) and Discard. Apply, Deploy to dev and the prod-channel branch need@typetorch/dev-server0.4.0, which is on the dev-server's main branch but not released yet (npm has 0.3.3).--claude-users <ids>lists the Roblox user ids allowed (remembered; before that, the owners intypetorch.json), so add a remote helper's id there too: a remote Studio prompts as the Roblox user it paired with, and serve pairs with remote-claude for them (nobody handles a remote-claude code). A remote-claude you start yourself in the repo works the same way (serve finds it through.typetorch/remote-claude.session.json). The tab sends Code only (the Live / Code pill just points you to the in-game DEV > Claude tab for Live) and has the in-game tab's other options: Toolbox, screenshots, logs and recent chats; the chat follows you between Edit and Play (serve keeps it per user while remote-claude runs). Replies stream to the tab over a WebSocket (/tt/claude/stream, through the tunnel too), so they show as Claude writes them.
Hot assets
Builders edit models and UI templates in the real place and publish it as usual; running servers pick up the new versions without a restart (the framework's hotAsset). Guide: Hot assets.
Marking: any instance with the string attribute
TypeTorchAsset= a key: lowercasea-z 0-9 / - _, at most 64 characters, unique in the place. A hot asset may not contain scripts (any LuaSourceContainer), may not sit inside another hot asset, and its parents' names may not contain/. A service can't be one. The sync refuses and lists every problem before uploading anything.typetorch assets sync:- Export: a Luau Execution task on the place's latest published version (the newest version the Assets API marks
published;--place-version <n>picks one) finds them, checks them, and serializes each withSerializationService:SerializeInstancesAsync. The task returns its metadata asReturnValuesand the bytes as binary output (enableBinaryOutput:TTA1+ the exports back to back, at most 256 MiB, downloaded from a presigned URL that never gets the key). The task drops the runtime's stamps (TypeTorchAssetId,TypeTorchAssetHash,TypeTorchAssetVersion,__typetorch_asset:*tags) from its own copy first; the place is never changed. - Diff: SHA-256 of each export, first 12 hex, against the lockfile: added, updated, removed, unchanged (and moved: same bytes, another parent).
- Upload (4 at a time): a new key gets a group-owned Model; a small placeholder (version 1) reserves the id, then the export is PATCHed on as version 2. A changed key gets a new version of the SAME asset (PATCH), so ids never change. Every uploaded copy carries
TypeTorchAssetId(number) andTypeTorchAssetHashon its root. Waits for moderation (--moderation-timeout, default 600 s). A removed key's asset stays on Roblox, and comes back with the same id if the key does. - Resolve: a second task calls
InsertService:GetLatestAssetVersionAsync(id)for each upload and loads that version to check itsTypeTorchAssetHash(retrying for up to 60 s), giving theassetVersionIdthatLoadAssetVersionneeds. - Write
typetorch.assets.lock.json(commit it), only when every upload and lookup worked: itsplaceVersionpromises that every entry matches the place at that version, and new servers adopt the place's own copies on that promise. A failed run writes nothing and exits 1; the next run reuses its approved uploads (assets.jsonl) and a placeholder it created. A newer published place with the same assets only updatesplaceVersion. From Studio (--studio-export <file>, what the plugin's buttons use): step 1 reads the plugin's export of the open place instead, so unpublished edits count. Changed entries then match no place version, so the lockfile recordsplaceVersion0 and new servers load every key whose copy differs; when nothing changed, the lockfile keeps its place version. The next sync from a published place records that version again. An export from another place is refused. - Report and print the next step.
--dry-runstops after the diff.--deploy <branch>then runstypetorch deploy --branch <branch>(same approval policy;--message,--propose,--proposed-by,--moderation-timeoutand the key file flags are passed on) when the Assets attribute changes. A prod-channel branch only takes clean builds, so--deployto one is refused while the sync changes the lockfile: sync, commit, then deploy.
- Export: a Luau Execution task on the place's latest published version (the newest version the Assets API marks
typetorch assets status: export + diff only (upToDatein--json). In--json, every change that isn'tunchangedcarries adiff(src/assetdiff.ts, what the plugin's Diff button shows):entriesof instancesadded,removedorchanged(path inside the asset, class, and for changed ones the properties with their old and new values, decoded for the common binary types; a chunk of another type that differs is named inundecoded),before/aftersummaries (instances, bytes, classes), bounded (30 entries, 8 properties each, values cut at 48 characters, the rest counted inmore/moreProps). The lockfile holds only a hash, so the previous version's bytes come from the state dir: everystatusandsynckeeps a copy of each export in.typetorch/hot-assets/<hash>.rbxmand drops the copies no lockfile entry or export refers to. Without a copy (synced on another machine, or before 0.15.0)baselineis"none", thenotesays so and only the export's summary is given; the next sync here fills the gap.typetorch assets list: the lockfile.Lockfile (
typetorch.assets.lock.json, keys sorted, one line per asset):json{ "v": 1, "placeVersion": 57, "assets": { "ui/shop": {"id":123456789012,"ver":44838191841145,"n":2,"hash":"abc123def456","realm":"replicated","path":"ReplicatedStorage/Assets/UI","className":"ScreenGui"} } }idasset id,verassetVersionId,nversion number,hashthe export's SHA-256 (12 hex),paththe parent's path,realmserverunder ServerStorage or ServerScriptService, elsereplicated.In the artifact: every build stamps the lockfile as the JSON attribute
Assetson the payload root and on itsServerfolder (the kernel drops the root; the framework readsServer):{"v":1,"placeVersion":57,"assets":{...}}, or{"v":1,"assets":{}}without a lockfile. An invalid lockfile stops the build. It is data only: the payload check (Folders and ModuleScripts) is unchanged. On prod it rides the signed payload; a rollback brings back older versions.Scopes (the assets key,
OPENCLOUD_ASSETS_KEYor the shared key):asset:read(also on the place: its version list),asset:write,universe.place.luau-execution-session:readanduniverse.place.luau-execution-session:write. A refused call stops the command and names the scope. Luau Execution allows 5 task creations per minute per key owner; a sync uses 2 (status1).
Cloud test (the pre-publish gate)
typetorch test --cloud [<artifact>] runs one Open Cloud Luau Execution task on the place's latest published version (what live servers run). Measured on the test place: 9-11 s (the task itself about 7 s).
InsertService:LoadAssetthe payload (the path live servers use) and the kernel's mount checks: a payload root, only Folders and ModuleScripts,KernelApinot above the place kernel's, the expectedArtifactId, and on a prod-channel branchChannelexactly"prod"(prod servers refuse anything else).- Mount the server tree in
ServerStorage.TypeTorch.Generationsand callServer.boot.boot(kernel)with a stub kernel (the real contract: persist, status, onMessage..., plustest = true); onInit runs inside it, onStart in spawned threads. It must return its stop function within the kernel'sREADY_TIMEOUT. requireevery ModuleScript underShared(5 s budget).- Run for
--seconds(default 5) whileScriptContext.Errorcollects every error, spawned threads included. - Stop it: the stop function within the kernel's
STOP_DEADLINE(5 s); then the tree is destroyed (scripts can't be disabled in a task, so the hard stop is emulated). Errors after the stop and instances the generation left behind are reported. - Boot a second generation of a fresh copy, run 1 s and stop it, as a hot swap would (
--no-swapskips it).
Fails (exit 1): the load and mount checks, a boot that throws or doesn't return, any error while it runs (onInit, onStart, Shared requires, the swap), a stop that throws, times out or logs "onStop threw". Warnings: errors after the stop (live servers' hard stop kills those threads), leftover instances, game warnings. Place scripts don't run in a task, but DataStores, MemoryStores and HttpService do: game code runs against real data, with no players. workspace:GetAttribute("TypeTorchTest") is true there, and the stub kernel has test = true, for code that must not run in the test.
In releases: deploy runs it on the approved upload, before the proposal or the message; promote before publishing; both always for prod-channel branches, and with --test elsewhere (rollbacks: --test only, they go to an earlier build). A pass of the same asset in the last 24 h (tests.jsonl) counts for promote and approve (--test runs it again). approve runs it before the y/N for a prod deploy or promote proposal that has no pass or skip recorded. --skip-test "<reason>" publishes without it; the reason goes into the proposal and the deploy log. Scopes: the assets key with universe.place.luau-execution-session:read + :write, and asset:read on the place (Luau Execution allows 5 task creations per minute per key owner; the CLI waits and retries on 429).
Fleet: servers, reports, alerts, --wait and automatic rollback
Kernel 0.3.2+ posts a heartbeat per server, one report per deploy outcome per server (results swapped, failed, rolled_back, skipped, booted) and alerts to the fleet API, a small service (the backend's fleet part, @typetorch/backend) you host. The CLI reads it:
- Setup:
typetorch backend setup --url https://<host>(see "Settings > Endpoint checks") writes the record'sbackend={url, key, analytics?: {flushSeconds, recordShare}}and, for kernels before 0.4 and frameworks before 0.4, the oldfleet={url, token}andanalytics(DuckDB ingest at<url>/v1/ingest, the same key; an existing section keeps its experiments) derived from it, with the game key fromTYPETORCH_API_KEY, pings the servers, sends the owner list (PUT /v1/access) and sets typetorch.json"backend": { "url": ... }(dropping CLI 0.8's"fleet", which is still read with a warning). Reads use the admin token inTYPETORCH_ADMIN_TOKEN; alerts the CLI posts use the game key (the backend refuses the admin token on its game routes). Both come from the environment or the game repo's.envand are never printed or passed to a child process. Without them,servers,reportandalertssay so in one line, and--waitis skipped with a note.typetorch fleet setupandtypetorch settings set analyticsmoved here (CLI 0.9; both print where). typetorch servers [--branch <b>] [--watch]: job, branch, artifact, applied seq, health (ok,failed,unverified,degraded), players, kernel, age (uptime), seen (last heartbeat);--watchredraws every 5 s. Reserved-server access codes are never printed.typetorch report <seq|artifact|latest> [--branch <b>]: counts per result, errors grouped with the servers that hit them, and the branch's servers still on an older seq. Exits 1 when a server failed or rolled back, and prints the rollback command.typetorch alerts [--follow] [--level info|warning|critical] [--since <minutes>]: the fleet's alerts (default the last 60 minutes);--followkeeps printing new ones.--wait [seconds]ondeploy,promote,rollbackandapprove(anddeploy --widen): on by default for prod-channel branches (90 s), off for dev (--waitturns it on,--no-waitoff). After the message it polls the reports every 5 s until every live server of the branch has reported the seq (or moved past it):- at about 30 s it re-sends the same message (same seq and signature; kernels ignore a seq they applied) when servers are still behind and haven't reported (kernels also poll the head every 60 s and retry failed loads);
- automatic rollback: when
--rollback-at(default: typetorch.jsonautoRollback.failedPct, else 20) % or more of the servers that tried the seq (skippedones don't count) reportfailedorrolled_back, with at least one failure, it rolls the branch back to the previous artifact through the same path astypetorch rollback(signed on prod-channel branches, with the key files on this PC). It decides early once the share can't drop below the threshold. It posts a criticalauto_rollbackalert (with the ingest token; otherwise it prints it), showsrolling back <branch> to <artifact> in 10 s: Ctrl+C keeps the new buildat a terminal (no wait without one), logs the reason on the rollback's deployment line (autoRollback) and on the proposal (anauto_rollbackevent), waits for the rollback's own reports, and exits 1.--no-auto-rollbackturns it off; - failures below the threshold: a red summary, the rollback command, exit 1;
- stalled servers alone never roll back: their JobIds are listed, a
server_stuckwarning alert is posted, and the command exits 0 with a warning.
Rollouts
--rollout <1-99> on deploy, promote and approve sends the message with ro: only servers whose bucket (djb2 of the JobId, mod 100) is below it swap; the others keep their artifact, and new servers boot the head. typetorch deploy --widen <1-100> [--branch <b>] re-sends the branch's live deploy, the same seq, with the new percentage (100 = every server, sent without ro); it asks y/N when typetorch.json approval applies, and logs to rollouts.jsonl. Dev-channel branches only: ro isn't covered by the signature, so prod servers ignore it on signed messages, and the CLI refuses --rollout for prod-channel branches (to try a build on some prod servers, use signed A/B pins: typetorch pin <artifact> --branch prod --pct <1-99>).
No GitHub Actions
TypeTorch doesn't use or ship GitHub Actions for games (owner decision: they are a common supply-chain risk). Game builds, cloud tests and deploys run from a developer's machine. (This repo's own CI and npm releases are the exception: see "Develop".) approve --import <dir> works in any automation you choose to run yourself.
Kernel deploy
typetorch kernel deploy:
runs
lune run scripts/check.luauin the kernel dir (--kernel, elsetypetorch.jsonkernel, elsenode_modules/@typetorch/kernel, else../kernel);checks the version (package.json
version=KERNEL_VERSIONinsrc/shared/Constants.luau, and the kernel API) and prints it with a content hash (place.project.json andsrc/, LF line endings). A git checkout must be clean and taggedv<version>(--allow-dirty,--allow-untagged);builds
.typetorch/place.rbxlwithKernelVersion,KernelHashandKernelCommitattributes, plus the signing trust rootsKeyAssetIdandFallbackPublicKeyfrom typetorch.json andBootstrapHeads(the JSON of the current prod-channel heads,{"<branch>":{"a","s","i"}}, from the local log), onServerScriptService.TypeTorchKernel(publishing refuses without the keys, or when the fallback key file doesn't match), and (kernel 0.3.6) the backup build: the current prod head's payload, whichdeployanduploadkeep in<state dir>/payloads/<artifactId>.rbxm(API keys can't download assets), checked (one Model, Folders and ModuleScripts only, Channelprod) and stamped withBackupArtifactId,BackupSeq,BackupBranch,BackupChannelandBackupAt, asServerStorage.TypeTorchBackup. Servers run it only when nothing else can (see the kernel's "Never an empty server"). It is a kernel slot, so every deploy replaces it. Without a kept payload for the prod head it warns and the place keeps the backup it has;--no-backupskips it.doctorshows the place's backup and its age (it warns past 30 days). When no backup is baked AND the default branch has no verified head (the kernel's DataStore heads, else the BootstrapHeads stamped now), publishing asks y/N first and refuses without a terminal unless--force: kernel 0.3.6+ with nothing to run moves every player out after 15 s and kicks after 3 bounces. Deploy the default branch first (servers without the kernel ignore it);patches the live place (the default,
--patch): it changes ONLY the kernel slots (theTypeTorch*children of services in the kernel'splace.project.json:ServerScriptService.TypeTorchKernel,ReplicatedStorage.TypeTorchKernelShared,ReplicatedFirst.TypeTorchKernelClient, and from kernel 0.3.6ServerStorage.TypeTorchBackup; every copy of each) and the service settings that project declares (HttpService.HttpEnabled;ServerScriptService.LoadStringEnabledonly with--loadstring, which sets it to true for remote-claude'srun_luauon a test place: without the flag a patch keeps the place's own value and--replace-placepublishes it off). The base is the place's newest version, which must be published (newer unpublished saves are refused and listed;--base publishedpatches the last publish and leaves them in version history,--base latestships them,--base <n>takes that version). A place without a kernel needs--install. An active Team Create session blocks the save. Roblox has no API-key route for downloading place files, so there are two ways to get at the place:--engine luau(the default without--place-file): no download. The slots are built alone into.typetorch/kernel-slots.rbxm(one FolderTypeTorchKernelSlots, a Folder per service, the slots; read back by the CLI's own reader) and sent once as a Luau Execution binary input (at most 100 MiB; valid 15 minutes).- A check task on
/versions/<base>deserializes it (SerializationService), reads the place's kernel, takes the outside manifest twice (every top-level child of every service that isn't a slot: tree shape, names, classes, attributes, tags, ObjectValue targets and script source hashes, plus the SHA-256 of its SerializeInstancesAsync bytes; bytes that differ between the two reads count by the descriptor only), swaps the slots, re-points ObjectValues that pointed into the old kernel (to the same path in the new one, or clears them, listed), applies the settings (one a task can't write stops it: set it once in Studio), takes the manifest again (anything changed stops it) and checks the slots and the identity attributes. It never calls SavePlaceAsync (the script doesn't contain it). The CLI compares the task's view of the new slots with its own reading of the.rbxm(instance and script counts, the script list hash), prints the summary and writes.typetorch/place-patches/<placeId>-kernel-<version>-luau.json. - y/N (or
--yes), then a check that nobody published since. - A save task on the same version repeats all of it (its outside manifest must equal the check's) and calls
AssetService:SavePlaceAsync(), which publishes (SaveWithoutPublish defaults to false). - The new version comes from the version list; a verify task on it checks the kernel identity, one copy of each slot (same counts and script list) and the outside descriptor.
Needs the place setting "Allow place to be updated using Save Place API" (Creator Hub > Creations > the experience > Places > the place > Permissions,
create.roblox.com/dashboard/creations/experiences/<universeId>/places/<placeId>/permissions; per place, off for places made in Studio) and the place key's Luau Execution scopes. The API key's owner needs edit rights on the place (group places: a role that can edit and publish the experience). Errors name the fix: the setting, Team Create, a task timeout (--timeout <s>, 30-300, default 300), the 100 MiB input limit (--no-backupleaves the backup build out), a rate limit (5 task creations a minute per key owner, 10 open tasks per place: 429s wait and retry). The base version stays in version history: undo withtypetorch kernel restore --version <base>(a task on that version calls SavePlaceAsync, which publishes it again as the newest version). Unverified until the first live run: the presigned upload is a PUT (content-type: application/octet-stream, then none), SavePlaceAsync's exact error texts, whether a task may read script sources and writeHttpEnabled.- A check task on
--place-file <file> --base <version>(the splice engine): a copy downloaded in Studio (File > Download a Copy), backed up to.typetorch/place-backups/<placeId>-v<n>.rbxl. The splice engine works on the binary chunks: every chunk of a class the kernel doesn't use is copied byte for byte; the script and folder classes are re-encoded with the game's own values moved as raw bytes; referents stay dense; references into the old kernel are re-pointed to the new instance at the same path, or cleared (listed).--engine lunere-encodes the whole place with Lune (rbx-dom) instead, which migrates some properties (Image -> ImageContent and others) and drops a few: only for when the splice engine refuses. It verifies twice: the CLI's binary reader (every instance outside the slots by path and class, no property chunk lost, the slots equal to the kernel build) and Lune (every subtree outside the slots equal, every reference pointing where it did, the slots and settings equal to the kernel build); writes.typetorch/place-patches/<placeId>-v<n>-kernel-<version>.rbxland a JSON report, prints a summary (kernel old -> new, scripts changed/added/removed per slot, settings, references, chunks copied), asks y/N (or--yes), checks that nobody published meanwhile, and publishes with the Place Publishing API (universe.place:write).--engine splicewithout--place-filetries the Asset Delivery download, which API keys can't do today (403, with this explanation).
--dry-rundoes everything except the save or publish.typetorch kernel restore <file> [--dry-run] [--yes]publishes a place file back: a backup (undo of a splice deploy), or a dry run's patched file (publish exactly what was inspected).--replace-place --yespublishes the whole kernel place instead and wipes Studio/Team Create content (the template/test place only). The place version before and after go tokernel-deploys.jsonl.
Backup build
Kernel 0.3.6 servers run ServerStorage.TypeTorchBackup only when the head, the last known good and the builds other servers run all fail. It should be at most one deploy behind:
- Automatic: after
typetorch deploy,approveorpromotepublishes build N+1 to the default (prod-channel) branch at your terminal, the place's backup becomes build N once N is proven healthy: the fleet API has reports for its seq with no failure or rollback, no server running it is failed or degraded, and it went out at leastbackup.healthyHoursago (typetorch.json"backup": { "refresh": "auto", "healthyHours": 3 };"refresh": "off"turns it off). It uses the luau engine with ONLY the backup slot (check task, save task with SavePlaceAsync, verify task; the rest of the place must not change). It never blocks or fails the deploy: unpublished saves, Team Create, the "Save Place API" setting, an unproven build, no kept payload, no place key, or a non-interactive run each print one line (backup not refreshed: ...). A rollback never moves the backup. - Now:
typetorch backup refresh [--build <x>](default: the prod head; y/N or--yes;--dry-run; an unproven build needs--force). doctorshows the place's backup build and its age.- Hot assets: each save publishes a new place version, and new servers adopt the place's hot-asset copies only for the version
typetorch.assets.lock.jsonnames. With a lockfile, the save stamps the new backup withHotAssetsPlaceVersion(the lock's version) andHotAssetsSavedAs(the version the save becomes) when the place's copies are the lock's: the lock was made on the base version, or the old backup vouched for it. Framework 0.5.3+ servers on exactly that version keep adopting the place's copies; any later publish has another version, so the claim ends there. When the claim can't be made (the lock is for another version), the automatic refresh is skipped with the fix (typetorch assets sync --deploy <branch>), andbackup refreshwarns before the y/N and after.typetorch kernel deploy(it bakes the backup build too) andtypetorch kernel restore --version <n>stamp the same way, warn before the y/N when they can't (a lock for another version;--no-backupor no prod head, so no backup to stamp), and print the outcome after. A place file publish (--place-file,--replace-place,kernel restore <file>) can't stamp: it warns after with the same fix. - A build taken down by moderation:
typetorch deploy --reupload <build>uploads the exact bytes kept at upload (<state dir>/payloads) as a NEW asset (uploads.jsonlreuploadOf= the old asset id), waits for moderation and deploys it like any deploy (the cloud test, the approval policy: prod = your y/N ortypetorch approve; a new seq, signed on prod). The artifact id stays; servers on the old asset swap to the new copy. Thentypetorch backup refreshif the backup was that build.
Develop
sh
bun install
bun test # unit tests
bun run typecheck
cd test-fixture && bun install && cd ..
bun test/fixture.e2e.ts # builds test-fixture/ in a temp git repo; deploy/rollback/promote/kernel only as --dry-run, no keys
bun run build # tsc -p tsconfig.build.json: src/*.ts -> dist/*.js (ESM for Node 20+; no Bun API in src)
bun run smoke # node scripts/smoke.mjs: the compiled bin and runtime under plain Node
bun run smoke:pack # + npm pack: file list, a scan for keys/local paths/user names, npx <tarball> --helpThe sources stay TypeScript (bun src/index.ts runs them directly). Bun-specific APIs are kept out of src/: child processes, PATH lookup, Windows .cmd scripts (Node won't spawn them directly: an npm shim runs its JS target with node; any other .cmd goes through cmd.exe with every argument quoted and escaped, and arguments with a double quote or a line break are refused), sleep and zstd live in src/runtime.ts, on Node's own modules. The build compiles with types: ["node"], so a Bun global in src/ doesn't compile. prepublishOnly runs the build, bun test and the pack smoke test.
Releases are tag-only. Nobody runs npm publish by hand: pushing a version tag publishes. Bump and tag on main with npm version <x.y.z> -m "release %s" && git push origin main --follow-tags; the tag (v*) starts .github/workflows/publish.yml, which checks that the tag matches package.json and that the version isn't on npm yet, runs npm publish --provenance (which runs prepublishOnly; a prerelease such as 0.13.0-next.0 goes to the next dist-tag, anything else to latest) and makes the GitHub release with generated notes. npm auth is trusted publishing (OIDC: this repo and publish.yml are a trusted publisher of @typetorch/cli on npmjs.com), so no npm token is needed; the workflow still passes an NPM_TOKEN repository secret if one is set. .github/workflows/ci.yml runs the typecheck, bun test (with Lune and Rojo), the build and the pack smoke test on every push to main and every PR.
bun run compile builds a single binary (release/typetorch).
License
MIT