Documentation
ReferenceArchitecture

CLI reference (tcli)

tcli is the Tabbify app-layer CLI. Its main job is tcli deploy: it hands your repository to Tabbify, which clones and builds it on its own machines and runs it as a Firecracker microVM. This is the same flow the quickstart drives through GitHub Actions — tcli deploy is what the tabbify-io/deploy action runs under the hood, and you can run it directly from your own terminal. The binary is tcli; all logic lives in the tabbify_cli library so it stays unit-tested.

There are five top-level subcommands:

  • deploy — build and deploy an app via the node API. The primary path.
  • node up — launch a supervisor node from a node.toml manifest.
  • secret — set/list/remove per-network secrets the node resolves into an app's [env] at deploy time (secret set/list/rm).
  • push — legacy: package an app directory and publish it to an S3 bucket.
  • self-update — replace the running tcli with the latest release.

tcli --version prints the build-stamped version (from the git v* tag).

deploy

Build and deploy an app through the node API. tcli deploy --remote is the production path: it hands the repo URL, ref, and a short-lived clone token to the node, which clones the repo, builds it into an OCI image on a Firecracker build sandbox, pushes the image to the mesh registry, and runs it as a Firecracker microVM. Your machine never builds or pushes anything.

tcli deploy --remote [<app-dir>] [flags]

<app-dir> defaults to the current directory. tcli reads tabbify.toml from it to fill in the app id, builder hint, deploy targets, and environment. It also resolves the repo coordinates automatically:

  • Repo URL--repo-url, else $GITHUB_SERVER_URL/$GITHUB_REPOSITORY (set inside a GitHub Action), else git remote get-url origin.
  • Git ref--ref, else $GITHUB_SHA, else git rev-parse HEAD.
  • Clone token--clone-token, else $TABBIFY_CLONE_TOKEN, else $GITHUB_TOKEN (the Action's per-run, repo-scoped token). The token stays on the host and is never handed into the build guest.

So inside a GitHub Action the command is just tcli deploy --remote; everything is inferred from the run. From your laptop, point it at a repo and authenticate. Mint the token in the console under Deploy tokens (pick a network → Manage tokensMint deploy tokenCopy; shown once):

export TABBIFY_TOKEN="your deploy token"
tcli deploy --remote \
  --repo-url https://github.com/you/my-app \
  --ref main \
  --clone-token "$GITHUB_TOKEN"
# deploy accepted: 0191e7c2-...
# app URL: https://app.tabbify.io/app/<your-id>/
# poll: https://api.tabbify.io/v1/deploy/0191e7c2-.../status

POST /v1/deploy is asynchronous: the node acks immediately with a deploy_id, then runs clone → build → push → deploy detached. tcli prints the id, your app's public URL, and the status URL to poll (GET /v1/deploy/<id>/status, which returns 202 while pending and 200 with the result when done). Once it succeeds the app is live at that URL.

FlagEnvDefault
--remoterequired today (Variant 2 is the only implemented build path)
--repo-url$GITHUB_SERVER_URL/$GITHUB_REPOSITORY, else git remote get-url origin
--ref$GITHUB_SHA, else git rev-parse HEAD
--tenantTABBIFY_TENANTtabbify (registry path prefix)
--app-uuid[app].id from tabbify.toml, else a deterministic v5 UUID of the repo URL
--builder[build].builder from tabbify.toml, else the node fans out to a builder-tagged supervisor
--providerinferred from the repo host (github / forgejo / generic)
--clone-token$TABBIFY_CLONE_TOKEN, else $GITHUB_TOKEN
--node-urlTABBIFY_NODE_ADDRhttps://api.tabbify.io
--tokenTABBIFY_TOKEN— (Bearer token for the node API; required for a real deploy)
--dry-runoff

--dry-run resolves the coordinates and prints the deploy request JSON instead of POSTing it — handy for confirming what would be sent. Deploy targets and per-target environment come from the [[deploy]] tables in tabbify.toml; with none declared, the node fans out to its Firecracker-capable supervisors. Field-by-field detail for the manifest is in the tabbify.toml manifest, and the end-to-end story is in the deploy pipeline.

Local-build deploy (Variant 1) is not implemented yet. Running tcli deploy without --remote errors out today. Building on Tabbify's own infrastructure with --remote is the supported path.

node up

Launch a supervisor node from a node.toml deployment manifest. This is for operators self-hosting a node, not for deploying apps.

tcli node up -f node.toml [--dry-run]

node up parses node.toml and materializes the docker run argv for tabbify-supervisor: it always wires the mesh transport (--device /dev/net/tun --cap-add NET_ADMIN), a persistent <name>-state:/var/lib/tabbify volume, and SUPERVISOR_NAME. The Firecracker capability adds --device /dev/kvm. --dry-run prints the command instead of running it.

# node.toml
[node]
name = "edge-fra-1"

[backend]
type    = "docker"          # how the supervisor process is launched on this host;
                            # only "docker" is implemented today
restart = "unless-stopped"

[artifact]
image = "registry.tabbify.io/tabbify/tabbify-supervisor:latest"

[capabilities]
firecracker = true          # pass /dev/kvm so the supervisor can run microVMs

# [mesh] is optional — omit it and the supervisor uses the compiled-in
# coordinator address (zero-config join). Set `coordinator` only to point at a
# different mesh.

[resources]
cpus      = 4
memory_mb = 8192

The backend.type = "firecracker" value is staged — it returns a clear "not yet implemented" error today; only the docker backend (how the supervisor process itself is launched on the host) builds an argv. For most hosts you don't need to write a node.toml by hand — the one-command installer (curl -fsSL https://get.tabbify.io/supervisor | sudo TABBIFY_JOIN_TOKEN='<jwt>' sh, token from the admin → your network → Add a node; the coordinator rejects a tokenless node with a 401) joins any systemd Linux machine to the mesh as a supervisor. See self-hosting a node for both paths.

secret

Manage per-network secrets the node holds in an encrypted store and resolves into an app's [env] at deploy time — so plaintext credentials never live in your repo, the tabbify.toml, or the event log (only the reference rides along).

printf '%s' "$GITHUB_CLIENT_SECRET" | tcli secret set GITHUB_CLIENT_SECRET
tcli secret list                    # names only, never values
tcli secret rm  GITHUB_CLIENT_SECRET

The value is read from stdin (never argv, so it isn't captured in your shell history or process list). Reference it from an app's manifest:

[env]
GITHUB_CLIENT_SECRET = "secret:GITHUB_CLIENT_SECRET"

At deploy the node rewrites each secret:NAME into the decrypted value before the env reaches the microVM. Secrets are scoped to the deploy network and selected via TABBIFY_NODE_ADDR + TABBIFY_TOKEN (same as deploy). A secret: reference with no store configured on the node fails the deploy loudly rather than shipping the app without its credential.

push (legacy)

push is a legacy fallback. It publishes a built artifact to an S3 bucket instead of letting Tabbify build from your repo. The supported path is deploy. push is documented here for operators maintaining older S3-based flows.

Package an app directory and publish it to an S3 bucket.

tcli push <app-dir> [--bucket <name>] [--region <r>] [--dry-run]

push resolves the app's identity from <app-dir>/tabbify.lock — absent mints a fresh UUID v7 (version 1), present reuses the id and bumps to latest + 1 — then writes the bumped lock back so you never hand-write the UUID. It uploads every file except tabbify.lock, target/, and .git/ to s3://<bucket>/apps/<uuid>/v<N>/, then writes the apps/<uuid>/latest text marker (the canonical version), building the OCI image first.

tcli push examples/hello-tabbify
# Resolved identity: uuid=0191e7c2-..., version=2
# Planning upload to s3://<bucket>/apps/0191e7c2-.../v2/
FlagEnvDefault
--bucketTABBIFY_APPS_BUCKETtabbify-apps
--regionTABBIFY_APPS_REGIONeu-central-1
--dry-runoff

--dry-run plans and prints the keys without any PutObject, but still advances tabbify.lock — versions are monotonic, so don't run it in a loop. A real push uploads via the default AWS credential chain and needs s3:PutObject on the target bucket.

Which manifest push reads

push prefers <app-dir>/tabbify.toml; if it's absent it falls back to the legacy manifest.toml and synthesizes a unified manifest from it. Either way the runtime resolves to Firecracker-from-OCI-image — there is a single runtime, so any legacy runtime.type in the file is ignored. When a tabbify.toml is present, push materializes a derived manifest.toml so the upload layer carries the correct lifecycle. Field-by-field detail is in the tabbify.toml manifest.

self-update

Replace the running tcli with the latest published release.

tcli self-update [--check] [--base-url <url>]

It reads the cli/latest JSON manifest, compares the build-stamped version against latest, downloads cli/v<VER>/tcli, verifies the manifest's sha256, atomically swaps the binary, and keeps the old one under ~/.tabbify/versions/<old>/tcli. --check only reports whether a newer version exists. --base-url (env TABBIFY_CLI_BASE_URL) overrides the release prefix.

tcli self-update --check
# update available: v1.3.0 -> v1.4.0 (run `tcli self-update`)

Next steps

New to the platform? Start with the quickstart for the full git push → build → live app round trip, then read the tabbify.toml manifest for the config deploy consumes and the deploy pipeline for what happens between your push and a running app.