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 anode.tomlmanifest.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 runningtcliwith 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), elsegit remote get-url origin. - Git ref —
--ref, else$GITHUB_SHA, elsegit 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 tokens → Mint deploy token → Copy; 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.
| Flag | Env | Default |
|---|---|---|
--remote | — | required 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 |
--tenant | TABBIFY_TENANT | tabbify (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 |
--provider | — | inferred from the repo host (github / forgejo / generic) |
--clone-token | — | $TABBIFY_CLONE_TOKEN, else $GITHUB_TOKEN |
--node-url | TABBIFY_NODE_ADDR | https://api.tabbify.io |
--token | TABBIFY_TOKEN | — (Bearer token for the node API; required for a real deploy) |
--dry-run | — | off |
--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 deploywithout--remoteerrors out today. Building on Tabbify's own infrastructure with--remoteis 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)
pushis a legacy fallback. It publishes a built artifact to an S3 bucket instead of letting Tabbify build from your repo. The supported path isdeploy.pushis 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/
| Flag | Env | Default |
|---|---|---|
--bucket | TABBIFY_APPS_BUCKET | tabbify-apps |
--region | TABBIFY_APPS_REGION | eu-central-1 |
--dry-run | — | off |
--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.