Documentation
ReferenceArchitecture

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