tabbify.io
Documentation
Product directionEDGE BOX
ReferenceArchitecture

The deploy pipeline

Tabbify accepts the same remote deployment from two entry points: a GitHub workflow or manual tcli deploy --remote. Both submit source coordinates to the node asynchronously. The node clones the selected revision, builds an OCI image, stores it in the platform registry, selects one eligible workload target, and boots a CPU-only Firecracker runner. Public ingress reaches the application at https://app.tabbify.io/app/<uuid>/ or a configured custom domain.

The remote path does not build or push the image on your laptop. See the quickstart for the GitHub walkthrough; this page is the in-depth version for both entry points.

GitHub trigger

The tabbify-io/deploy GitHub Action can run on every push and submit the remote 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 tokens → Mint deploy token → Copy; 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 the repository URL, git ref, clone token, tenant, manifest, and optional selected target. The node acknowledges with a deploy ID and continues asynchronously:

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. select one eligible workload target, using the requested supervisor or default
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. start one Firecracker runner on the selected target at derive_app_ula(uuid)
7. apply the configured health gate before activation

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 stable for the deployment source. 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 repository keeps its address across pushes. If the manifest has no [[deploy]] block, the node selects the default eligible supervisor.

One runtime: Firecracker

Tabbify ships a single public 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. [build].kind = "docker" describes the OCI build; runtime-selection keys in older manifests are inert. See Runtimes for the full story.

The target is capability-checked against the supervisor's roster tags before any dispatch: a supervisor must carry the firecracker tag to receive a runner, and a build-only host is not handed an app it cannot boot.

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.

Build, target resolution, capability, registry, spawn, and health failures are reported in deploy status. 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, applies the configured health gate, and hands back the app_ula. The address is derived from the app UUID rather than the host, so later deployments keep the same route.

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 the runner on the selected supervisor; you do not call it by hand.

Status and release limits

The status operation reports the current phase, elapsed time, health outcome, and, when present, a bounded tail of local application output. The tail is for diagnosis and is not durable platform logging.

The platform does not currently provide an automated rollback operation or a durable deployment history. To recover, submit a known-good source revision as a new deployment and follow its status. Passing the candidate health gate reduces risk, but it is not a guarantee that a release will be uninterrupted.

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