Documentation
ReferenceArchitecture

The deploy pipeline

Tabbify takes an app from source to a running mesh peer through one path: git push → GitHub Actions → the node's POST /v1/deploy. The node clones your repo, builds it on a builder supervisor, pushes the resulting image to the in-mesh registry, and deploys it as a Firecracker runner on a UUID-deterministic mesh address. The node then proxies public traffic to it at https://app.tabbify.io/app/<uuid>/.

Your laptop never builds or pushes anything — it only does a git push. See the quickstart for the four-file walkthrough; this page is the in-depth version.

The trigger: GitHub Actions

You don't talk to the node directly. The tabbify-io/deploy GitHub Action runs on every push and drives the deploy for you:

- uses: actions/checkout@v4
- uses: tabbify-io/deploy@v1
  with:
    token: ${{ secrets.TABBIFY_TOKEN }}

The action installs tcli and runs tcli deploy --remote. Two pieces of auth flow through it, and they are different things:

  • token is your TABBIFY_TOKEN — the bearer credential the node checks on POST /v1/deploy. Mint it in the console under Deploy tokens (pick a network → Manage tokensMint deploy tokenCopy; shown once), then add it once as a repository secret.
  • clone-token defaults to the Action's automatic ${{ github.token }} — a per-run, repo-scoped token the node uses to git clone your repo. There is no GitHub App to install and no webhook to register; the clone token lives on the build host and never enters the build guest.

What the node does

tcli deploy --remote POSTs to /v1/deploy with the repo URL, git ref, clone token, tenant, and (optionally) explicit deploy targets pulled from your tabbify.toml. The node acknowledges with 202 {deploy_id} immediately and runs the pipeline in a detached tokio::spawn:

1. resolve the in-mesh registry (registry ULA from the roster)
2. resolve a builder supervisor (from [build].builder, the request, or the roster)
3. resolve every deploy target to a supervisor ULA — fail fast if unresolvable
4. clone the repo at the pushed ref (using the clone token) and build an OCI image
5. push the image to the in-mesh Zot registry (/v2)
6. fan out the artifact ref to each target; each spawns a Firecracker runner
   on derive_app_ula(uuid)

Because the response is async, you poll for the outcome:

GET /v1/deploy/<deploy_id>/status   # 202 while pending, 200 with the result

The app UUID is deterministic. The CLI prefers an explicit --app-uuid, then [app].id in tabbify.toml, and finally falls back to uuid_v5(NAMESPACE_URL, repo_url) — so the same repo keeps its address (and its data) across pushes. If the manifest has no [[deploy]] block, the node fans out to every run-capable supervisor in the roster.

One runtime: Firecracker

Tabbify ships a single runtime. Every app is built from a Dockerfile into an OCI image, converted to an ext4 rootfs, and booted as a Firecracker microVM running the image's entrypoint. There is no runtime to choose: [build].kind is always "docker" (the OCI image), and any legacy [runtime].type or [[deploy]].runtime field in an older tabbify.toml parses fine but is ignored. See Runtimes for the full story.

Targets are still capability-checked against each supervisor's roster tags before any dispatch: a supervisor must carry the firecracker tag to receive a runner, and a docker-only build host won't be handed an app it can't boot. What's gone is the old multi-runtime selection complexity — a target now just resolves to a supervisor ULA.

Deploying from your own terminal

The GitHub Action is just a wrapper. You can run the exact same pipeline from any terminal with tcli:

tcli deploy --remote \
  --repo-url https://github.com/tabbify-io/quickstart \
  --ref main \
  --clone-token "$GITHUB_TOKEN"

tcli reads TABBIFY_TOKEN for the node bearer auth and posts to https://api.tabbify.io/v1/deploy by default. Or hit the endpoint directly with your Tabbify API token:

curl -X POST -H "Authorization: Bearer $TABBIFY_TOKEN" \
  https://api.tabbify.io/v1/deploy \
  -d '{"repo_url":"https://github.com/tabbify-io/quickstart",
       "git_ref":"main","tenant":"tabbify",
       "app_uuid":"0191e7c2-1111-7222-8333-444455556666",
       "clone_token":"<github-token>",
       "manifest_toml":"[app]\nname=\"quickstart\"\n[build]\nkind=\"docker\"\n[runtime]\nport=8080\n[routes]\ndynamic_prefixes=[\"/\"]\n",
       "targets":[{"supervisor":"thinkpad"}]}'

manifest_toml carries your raw tabbify.toml so the node applies [runtime] (including port) and [routes] on spawn. Omit it and the app gets the defaults (port 8080) — fine for an 8080 image, a crash-loop for anything else. tcli deploy sends this for you; only a hand-rolled curl needs it explicit.

Per-target failures are collected into results and do not fail the call — partial deploys are reported honestly. Only a build error, an unresolvable builder or target, a capability mismatch, or a missing registry is fatal. See the registry for the artifact store and auth for how tokens are issued.

Where it lands: the runner

The pipeline terminates at a runner. supervisord is pure control plane: it spawns a detached tabbify-runner, waits for its control socket to go healthy, and hands back the app_ula. Because runners are detached, killing the supervisor never kills the workload — a restart re-adopts living runners with no blip. The address is metadata-free: derive_app_ula(uuid) is host-independent, so an app keeps its ULA when it migrates between supervisors.

You can inspect the live mesh from the node:

curl -H "Authorization: Bearer $TABBIFY_TOKEN" \
  https://api.tabbify.io/v1/topology

That topology view is for visibility only — addressing is always the UUID hash. A supervisor also exposes POST /v1/apps/{uuid}/start for advanced or manual control, but in the normal GitHub Actions flow the deploy pipeline starts runners on the target supervisors for you; you never call it by hand.

Status: the full pipeline is verified live on AWS — git push → GitHub Actions → tcli deploy --remote/v1/deploy → builder supervisor → mesh registry → Firecracker runner, reachable at https://app.tabbify.io/app/<uuid>/.

See the runtime for the runner lifecycle, routing for the address scheme, and the CLI reference for tcli.