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. Bothhttps://api.tabbify.io/app/<uuid>/pathand the shorterhttps://api.tabbify.io/<uuid>/pathreach the same app. - Control —
/v1/*. These requireAuthorization: Bearer <TABBIFY_TOKEN>, the API token issued to your Tabbify account./healthand 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:
git pushtriggers the deploy pipeline; the supervisor spawns a runner that binds[app_ula]:8730.- 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.