Documentation
ReferenceArchitecture

Routing & public access

Every Tabbify app gets its own address on the private mesh, derived from its UUID. The public node is the one component with a public listener — it joins the mesh and proxies external requests to those addresses. This page covers both halves: the per-app address and the public edge in front of it.

Per-app addresses

An app's mesh address is a deterministic IPv6 ULA derived from its UUID via BLAKE3, in the fd5a:1f02::/32 block:

fn derive_app_ula(app_uuid: Uuid) -> Ipv6Addr {
  let b = blake3::hash(app_uuid.as_bytes());
  let b = b.as_bytes();
  Ipv6Addr::new(
    0xfd5a, 0x1f02,
    u16::from_be_bytes([b[0], b[1]]),
    u16::from_be_bytes([b[2], b[3]]),
    u16::from_be_bytes([b[4], b[5]]),
    0, 0, 1,
  ) // fd5a:1f02:<48-bit hash>::1
}

Because the hash depends only on the UUID, the supervisor and the node compute the same address independently — no lookup table, no per-request supervisor query. The address is host-independent and migration-safe: move an app to another supervisor and it keeps fd5a:1f02:…::1. Each app's runner binds [app_ula]:8730, and the node dials that address directly over the mesh.

The public edge

tabbify-node is the one component with a public listener. It joins the mesh tagged node, which installs /128 routes to the mesh-advertised app ULAs so it can dial apps directly. In front of it sits CloudFront, which terminates HTTPS and gives you a single public hostname — you never touch a raw IP or a port.

The node exposes two kinds of routes:

  • App-serving/app/{uuid}, /app/{uuid}/{*rest}, and the bare /{uuid} and /{uuid}/{*rest} aliases. These are public: they carry no bearer gate, because this is the edge that fronts your deployed app to the open internet. Both https://api.tabbify.io/app/<uuid>/path and the shorter https://api.tabbify.io/<uuid>/path reach the same app.
  • Control/v1/*. These require Authorization: Bearer <TABBIFY_TOKEN>, the API token issued to your Tabbify account. /health and the docs (/openapi.json, /swagger-ui) are public too.
# Reach a deployed app — public, no auth needed
curl https://app.tabbify.io/app/<uuid>/some/path

# Same app via the node's API host (the bare /<uuid> alias also works)
curl https://api.tabbify.io/<uuid>/some/path

# A control call — requires your Tabbify API token
curl https://api.tabbify.io/v1/apps \
  -H "Authorization: Bearer $TABBIFY_TOKEN"

App-serving routes derive the ULA and stream the request to [app_ula]:8730 — bodies are not buffered, hop-by-hop headers are stripped both ways. An unreachable app (no /128 route installed yet) returns 502; a malformed UUID returns 400.

Getting a public URL

You don't wire any of this up by hand. When you deploy from a git push, the pipeline clones and builds your repo, runs it as a Firecracker microVM, and the runner binds its derived app_ula. The node already knows how to reach that address, so the app is live the moment the runner is healthy:

  1. git push triggers the deploy pipeline; the supervisor spawns a runner that binds [app_ula]:8730.
  2. A request to https://app.tabbify.io/app/<uuid>/ is routed: CloudFront → node → mesh → runner.
browser → CloudFront (HTTPS) → node → WireGuard tunnel → [app_ula]:8730

The hard part — finding the host — is solved by the deterministic address: the node never looks an app up in a table, it just hashes the UUID and dials. The remaining frontier is NAT traversal between mesh peers: direct connections fall back to hole-punching and then a DERP relay (relay.tabbify.io) when peers can't reach each other directly. See routing internals in the node and self-hosting for the supervisor side.