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. The wire struct is vendored identically across
tcli, the supervisor, and the
node, so the fields mean the same thing everywhere they are read.
There is exactly one runtime: an OCI image, built from your Dockerfile, booted
as a Firecracker microVM. You don't pick a runtime — every app
builds a Docker/OCI image and runs as Firecracker. The manifest still tolerates a
few legacy fields ([runtime].type, kind = "wasm", [[deploy]].runtime) for
wire-compatibility with old files, but they are ignored — see
Gotchas.
tabbify.toml is the unified manifest read by every component. A legacy
manifest.toml continues to parse for backward compatibility — see
Gotchas for precedence.
A minimal manifest
The smallest manifest declares a name and a build kind:
[app]
name = "hello"
[build]
kind = "docker" # always "docker" — builds your Dockerfile into an OCI image
Identity: [app]
[app] carries the app's identity. You only write name (and optionally
description); the UUID is yours to set, and if you leave it out it's minted for
you on first deploy and tracked in tabbify.lock.
[app]
id = "0191e7c2-1111-7222-8333-444455556666" # your app's permanent address
name = "booking-site"
description = "Hugo's barbershop booking"
The id is a UUID v7. Generate one yourself (uuidgen) and paste it in, or let
tcli mint one once and write it to tabbify.lock. That UUID deterministically
derives the app's private mesh address and its public URL — see
Private Mesh and Routing.
Build: [build]
[build].kind is a frozen enum whose only value is docker: the build always
produces an OCI image from your Dockerfile. (There is one runtime, so there is
one build kind. A legacy kind = "wasm" is tolerated and resolves to docker.)
[build]
kind = "docker" # always "docker" — builds an OCI image
context = "."
dockerfile = "Dockerfile"
builder = "thinkpad" # optional: which supervisor compiles the image
builder optionally names the supervisor that compiles the image; leave it unset
and Tabbify picks a capable builder for you.
Runtime, ports, and lifecycle: [runtime]
[runtime] carries only lifecycle and resource limits — not a runtime choice.
Every app runs as a Firecracker microVM; there is nothing to select.
[runtime]
lifecycle = "on_request" # always_on | on_request
idle_timeout_sec = 300
memory_mb = 512
vcpus = 1
Legacy field: old manifests may carry
[runtime].type = "docker" | "firecracker" | "wasm-http". It's tolerated for wire-compatibility but ignored — the runtime is always Firecracker. There is no runtime selection at build or deploy time.
-
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. -
port — the guest port your app listens on inside the microVM. A runner binds the app on
[app_ula]:8730over the mesh and the node proxies/app/<uuid>to it; the runner forwards that mesh side to your app's port. It defaults to8080, so an image that listens on 8080 needs nothing. If your app listens elsewhere, declare it — otherwise the readiness probe hits the wrong port and the app crash-loops:[runtime] port = 3000
Deploy targets: [[deploy]]
Each [[deploy]] block places the app on one supervisor and may inject env for
that target only:
[[deploy]]
supervisor = "thinkpad"
[deploy.env]
LOG_LEVEL = "debug"
[[deploy]]
supervisor = "ec2-prod"
If you omit [[deploy]] entirely, Tabbify falls back to a default supervisor —
which is exactly what the quickstart does. A legacy
[[deploy]].runtime key is tolerated and ignored (there is no runtime to
override).
The multi-target deploy loop (per-target placement 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. A credential lives in the
encrypted per-network store (tcli secret set NAME, value via stdin) and the
manifest references it by name — the node rewrites each secret:NAME into the
real value at deploy time, so the plaintext never touches the manifest, the event
log, or the prompt:
[env]
STRIPE_KEY = "secret:STRIPE_KEY" # resolved from the store at deploy
Sandbox capability bounds (allowed hosts, event segments) are declared and
enforced by the supervisor; see Services & capabilities and the
secret CLI.
Gotchas
tabbify.tomltakes precedence over a legacymanifest.tomlif both exist.- Unknown and legacy fields are silently ignored (
#[serde(default)], nodeny_unknown_fields). That's how old manifests stay compatible — but it also means a typo likelifcyclefalls back to the default rather than erroring. Watch your spelling. - Legacy runtime selection is inert:
[runtime].type,kind = "wasm", and[[deploy]].runtimeall parse but are ignored. Every app runs as Firecracker.
Once your manifest is ready, deploy it. The simplest path is a git push — put
the repo on GitHub with the deploy workflow and Tabbify clones, builds, and runs
it for you; see the quickstart. To deploy from your own
terminal instead, run tcli deploy --remote, which hands your repo and commit to
the node to build and run remotely; see the CLI reference.