Documentation
ReferenceArchitecture

The deploy pipeline

Tabbify has two ways to get an app from source to a running mesh peer. A git push fires a webhook that builds in-mesh and pushes to the registry; a tcli push packages a directory and publishes it to S3. Both end the same way: a supervisor spawns a detached runner on a UUID-deterministic mesh address, and the node proxies traffic to it.

Entry point 1: git push (webhook build)

Point a GitHub App at POST /v1/webhooks/github on your node. There is no bearer token here — the HMAC X-Hub-Signature-256 header is the auth, verified in constant time against GITHUB_WEBHOOK_SECRET. With no secret set, the endpoint returns 503; a bad signature returns 401.

On a verified push, the node acknowledges with 202 immediately (GitHub's delivery timeout is 10s) and runs the deploy in a detached tokio::spawn. From there:

1. mint a GitHub installation token (App private key flow)
2. fetch tabbify.toml at the pushed SHA
3. pick a builder supervisor; it git-clones + docker builds
4. docker push -> in-mesh Zot registry (/v2)
5. fan out the artifact ref to every [[deploy]] target
6. each target's supervisor spawns a runner on derive_app_ula(uuid)

The app UUID is deterministic — 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 falls back to a single default supervisor with a Docker build and runtime, which is exactly what hello-deploy exercises.

You can trigger the same pipeline by hand with a bearer key:

curl -X POST -H 'Authorization: Bearer <node-key>' \
  http://localhost:8090/v1/deploy \
  -d '{"repo_url":"https://github.com/tabbify-io/hello-deploy",
       "ref":"main","tenant":"tabbify-io",
       "app_uuid":"0191e7c2-1111-7222-8333-444455556666",
       "targets":[{"supervisor":"thinkpad","runtime":"firecracker"}]}'

Per-target failures are collected into results and do not fail the call; only a build error, an unresolvable builder, a capability mismatch, or a missing registry is fatal. Runtime requests are capability-checked against each supervisor's roster tags before any dispatch — ask firecracker of a docker-only host and the whole deploy aborts. See Runtimes and the registry.

Status: verified live on AWS 2026-05-28 with tabbify-io/hello-deploy — image landed in the registry, runner spawned on fd5a:1f02:d5c0:ee4c:8978::1.

Entry point 2: tcli push (S3 artifacts)

tcli push <dir> skips the in-mesh build. It reads manifest.toml, mints (or reuses, via tabbify.lock) a UUID v7, and uploads every file to the public-read tabbify-apps bucket:

apps/<uuid>/v<N>/manifest.toml        # stamped with app.id
apps/<uuid>/v<N>/<your files...>
apps/<uuid>/v<N>/image.tar.gz         # only for [build].kind = "docker"
apps/<uuid>/latest                    # body = "<N>"

For a Docker build kind, tcli runs docker build + docker save | gzip locally and ships the tag tbf-img-<uuid>-v<N>, so the supervisor docker loads it warm instead of rebuilding. WASM apps ship just the .wasm. Use --dry-run to print the plan without uploading or invoking Docker.

A supervisor started with --app <uuid> fetches apps/<uuid>/ from S3, binds derive_app_ula(uuid):8730, and advertises to the coordinator; the node's joiner installs the mesh route, and curl node:8090/app/<uuid> lands directly on the app over WireGuard.

Status: verified live on AWS 2026-05-25 (Phase-1 runbook) — hello-tabbify WASM served end-to-end through the mesh.

Where they converge

Both paths terminate at the runner. supervisord is pure control plane: POST /v1/apps/{uuid}/start spawns a detached tabbify-runner, waits for its control socket to go healthy (30s), and returns 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.

curl -H 'Authorization: Bearer <node-key>' http://localhost:8090/v1/topology

That topology view is for visibility only — addressing is always the UUID hash. See the runtime for the runner lifecycle, routing for the address scheme, and the CLI reference for tcli.