The tabbify.toml manifest
tabbify.toml declares everything about an app in one file: how it builds, how it
runs, and where it deploys. It sits at the root of your app directory next to your
Dockerfile (or app.wasm). The wire struct is vendored identically across
tcli, the supervisor, and the
node, so the fields mean the same thing everywhere they are read.
The unified tabbify.toml is the target manifest format, and migration is in
flight: today tcli push still primarily reads the legacy manifest.toml, the
supervisor parses tabbify.toml, and the node reads it on a GitHub webhook. A
legacy manifest.toml continues to work — see Gotchas for precedence.
A minimal manifest
The smallest manifest declares a name, a build kind, and a runtime:
[app]
name = "hello"
[build]
kind = "wasm"
[runtime]
type = "wasm-http"
Identity: [app]
[app] carries the app's identity. You only write name (and optionally
description); the UUID is minted for you on first tcli push and tracked in
tabbify.lock.
[app]
id = "0191e7c2-1111-7222-8333-444455556666" # stamped by tcli, do not hand-edit
name = "booking-site"
description = "Hugo's barbershop booking"
The id is a UUID v7. tcli mints it once, writes it to tabbify.lock, and
stamps it into the manifest body before uploading to S3. That UUID is also what
deterministically derives the app's private mesh address — see
Private Mesh.
Build: [build]
[build].kind is a frozen enum: docker or wasm. It selects the build
artifact, independent of how the app later runs.
[build]
kind = "docker" # docker | wasm
context = "."
dockerfile = "Dockerfile"
builder = "ec2-prod" # which supervisor builds the image
builder names the supervisor that compiles the image. There is no firecracker
build kind — to run under Firecracker you build with kind = "docker" and override
the runtime per target (below).
Runtime, ports, and lifecycle: [runtime]
[runtime].type is a frozen enum: docker, firecracker, or wasm-http.
Crucially, runtime is chosen at deploy time, not baked at build time — the same
Docker image can run as a container on one host and a Firecracker microVM on
another.
[runtime]
type = "docker" # docker | firecracker | wasm-http
lifecycle = "on_request" # always_on | on_request
idle_timeout_sec = 300
memory_mb = 512
vcpus = 1
For WASM apps you also point at the entry component and set a fuel budget:
[runtime]
type = "wasm-http"
entry = "app.wasm"
fuel_per_request = 1000000000
memory_mb = 64
- lifecycle —
always_onspawns at registration and stays up;on_requestlazy-spawns on first request and idle-stops afteridle_timeout_sec. An app started explicitly via the API is pinned and is never idle-reaped. - ports — apps don't pick a public port. A runner binds its app on
[app_ula]:8730over the mesh, and the node proxies/app/<uuid>to it. For Firecracker the guest listens internally on8080(SUPERVISOR_FC_APP_PORT). See Routing & public access.
Deploy targets: [[deploy]]
Each [[deploy]] block places the app on one supervisor and may override the
runtime and inject env for that target only:
[[deploy]]
supervisor = "thinkpad"
runtime = "firecracker" # overrides [runtime].type for this target
[deploy.env]
LOG_LEVEL = "debug"
[[deploy]]
supervisor = "ec2-prod"
runtime = "docker"
A supervisor only accepts a runtime it advertises. Supervisors announce capability
tags — wasm-http always, docker if the daemon is reachable, firecracker if
/dev/kvm is available — and the node validates the requested runtime against
those tags before placement, rejecting unsupported combinations with a 400.
The multi-target deploy loop (per-target runtime override and [deploy.env]
merge) is designed and partially wired; treat [[deploy]] as the intended shape
while the node's deploy path is finished.
Secrets and capabilities
tabbify.toml does not carry plaintext secrets. Credentials live in the encrypted
secret store and are injected as environment at runtime — never written into the
manifest, the event log, or the prompt. Sandbox capability bounds (allowed hosts,
event segments) are declared and enforced by the supervisor; see
Services & capabilities.
Gotchas
tabbify.tomltakes precedence over a legacymanifest.tomlif both exist.- Unknown fields are silently ignored (
#[serde(default)], nodeny_unknown_fields), so a typo likelifcyclefalls back to the default rather than erroring. Watch your spelling. - The S3
latestmarker is the canonical version number; theversionfield in the manifest is display-only and ignored by the supervisor.
Once your manifest is ready, publish with tcli push and follow
the deploy pipeline.