Services
A service in Tabbify is just an app that talks to the outside world — an HTTP API,
a chat bot, a job runner that calls a third party. There is nothing special about
its packaging: a service is the same thing you ship in the
quickstart — a repo with a Dockerfile and a tabbify.toml,
deployed by a git push. Tabbify builds it into an OCI image and runs it as a
Firecracker microVM. What makes it a service is the
configuration around it: the environment it needs, how long it lives, and where
it gets built.
Apps reach each other and the world over the private mesh — each running app is a mesh peer with its own address, and requests are routed directly to that address. There is no event bus and no message broker between apps; one service calls another over plain HTTP across the mesh, just as the public node proxies inbound traffic to your app.
The manifest
A service uses the same tabbify.toml as any other app.
The runtime is always Firecracker — you don't pick one — so the manifest only
describes what to build, how long to keep it alive, and where requests go:
[app]
name = "my-service"
description = "wraps an external API"
# no `id` needed — Tabbify assigns a permanent address and prints it on deploy
[build]
kind = "docker" # build the Dockerfile into an OCI image
[runtime]
lifecycle = "always_on" # a service that must always be reachable
idle_timeout_sec = 300 # (only matters for on_request)
memory_mb = 512
vcpus = 1
[routes]
dynamic_prefixes = ["/"] # send every path to your app
Two lifecycle modes shape how a service runs:
on_request— the app sleeps when idle and wakes on the next request. Cheap, but the first request after an idle period waits for the VM to boot. Good for a bot or an endpoint that is hit occasionally.always_on— the app is spawned on deploy and kept running. Use this for a service that must respond immediately, or that does background work and can't afford to sleep.
There is no capability list, no permission table, and no [secrets] block in the
manifest. A Phase-1 app is sandboxed by the microVM boundary, not by a
manifest-declared capability bundle. (Capability tags exist, but they describe a
supervisor's abilities — builder, docker, firecracker — not what an app
may do; see Self-hosting.)
Configuring an external dependency
To wrap a third-party service — calling an API, talking to a database, driving a
bot — your app reads its configuration from environment variables, the way any
container does. You provide those in tabbify.toml:
[env]
UPSTREAM_URL = "https://api.example.com"
LOG_LEVEL = "info"
[env] applies to every placement of the app. If you deploy the same service to
more than one supervisor, a [[deploy]] block can override individual keys for a
specific placement:
[[deploy]]
supervisor = "ec2-prod"
[deploy.env]
LOG_LEVEL = "debug"
For a secret (an API key, a DB password), don't put the value in
tabbify.toml — store it in Tabbify's per-network secret store and reference it
by name. The node keeps secrets in an encrypted, per-network store and resolves
each secret:NAME reference into the real value at deploy time, so the plaintext
never lives in your repo, the manifest, or the event log:
printf '%s' "$DB_PASSWORD" | tcli secret set DB_PASSWORD # value via stdin
[env]
DB_URL = "postgres://app@db.internal/app" # non-secret, plain
DB_PASSWORD = "secret:DB_PASSWORD" # resolved from the store
At deploy the node rewrites secret:DB_PASSWORD into the decrypted value before
the env reaches the microVM; tcli secret list shows names only. A secret:
reference with no store configured fails the deploy loudly rather than shipping
the app without its credential. See the CLI reference.
Pinning a build host
A service often needs to be built somewhere specific — a machine with the right
architecture, or your own self-hosted box. By default the node picks a build host
from the fleet; to pin one, name it in [build]:
[build]
kind = "docker"
builder = "buildbox" # supervisor display name (or its mesh address)
This only chooses where the image is built. The app still runs as a Firecracker microVM wherever it is deployed.
Service-to-service calls
Because every running app is a mesh peer with its own address, one service can call another directly over the mesh — no gateway, no event log in between. The routing page covers how addresses are derived and how the public node forwards inbound requests. For the mesh itself — addressing, NAT traversal, the relay — see Private mesh.
Where to go next
- tabbify.toml — every field of the manifest, including
[env]and[[deploy]]. - The runtime — how an OCI image becomes a running microVM.
- Routing & public access — how requests reach a running service.
- Self-hosting — run your own supervisor and pin builds to it.