Documentation
ReferenceArchitecture

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.

  • lifecyclealways_on spawns at registration and stays up; on_request lazy-spawns on first request and idle-stops after idle_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]:8730 over the mesh and the node proxies /app/<uuid> to it; the runner forwards that mesh side to your app's port. It defaults to 8080, 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
    

    See Routing & public access.

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.toml takes precedence over a legacy manifest.toml if both exist.
  • Unknown and legacy fields are silently ignored (#[serde(default)], no deny_unknown_fields). That's how old manifests stay compatible — but it also means a typo like lifcycle falls back to the default rather than erroring. Watch your spelling.
  • Legacy runtime selection is inert: [runtime].type, kind = "wasm", and [[deploy]].runtime all 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.