Documentation
ReferenceArchitecture

Private Mesh

Every Tabbify node and every app lives on a private WireGuard overlay — a Tailscale-style mesh built on userspace boringtun. Peers get stable IPv6 ULA addresses, establish encrypted tunnels directly to each other, and never expose a public app port. This is how the node, supervisors, and your apps reach one another.

Coordinator and joiner

The mesh has two roles. The coordinator is a stateless HTTP control plane: it registers peers, hands out ULAs, filters who can see whom by ACL, and streams roster changes over SSE. The joiner runs inside every node — it opens a TUN device, registers, and establishes a WireGuard session to each visible peer.

# Coordinator (production: mTLS + ACL + join-token validation)
tabbify-mesh-coordinator --bind 0.0.0.0:8888 \
  --tls-cert ./coord.crt --tls-key ./coord.key --tls-ca ./ca.crt \
  --policy-file ./acl.json --auth-url http://localhost:8080

# Joiner — the coordinator EIP is baked in, so this is zero-config
tabbify-mesh join --name supervisor-01 --tag supervisor --tag firecracker \
  --tls-cert ./peer.crt --tls-key ./peer.key --tls-ca ./ca.crt \
  --join-token $MESH_JOIN_TOKEN

The production coordinator (http://3.124.69.92:8888) is the default in every joiner binary. Override it only for local dev with --coordinator or TABBIFY_MESH_COORDINATOR.

Peer and per-app ULAs

Addresses come from two deterministic blocks. Peers get fd5a:1f00::/32. Apps get their own address derived from the app UUID — fd5a:1f02:<blake3(uuid)[0:6]>::1 — computed identically by supervisor and node, so an app keeps the same address across restarts and migrations. Supervisors advertise the app-ULAs they host on each heartbeat (POST /v1/mesh/heartbeat), and receiving peers install /128 routes so dialing [app_ula]:8730 reaches the right host.

{"peer_id":"<uuid>","ula":"fd5a:1f00:0000:0001::1","display_name":"supervisor-01",
 "tags":["supervisor"],"hosted_app_ulas":["fd5a:1f02:44a5:240b:121a::1"]}

App-ULA routing is wired in the joiner's routing layer; full supervisor-to-node integration is tracked as a separate task, so today the node still resolves the hosting supervisor before dialing.

NAT traversal

The coordinator does reflexive endpoint discovery: it pairs the observed public IP (from the HTTP source) with the reported wg_listen_port (from the request body — note these are different ports) to synthesize a reachable endpoint. That handles public IPs and port-preserving cone NATs with no manual config. For two cone-NAT peers it broadcasts a HolePunch event over the GET /v1/mesh/peers/stream SSE feed and both sides fire simultaneous handshake bursts. This is verified live (home NAT to cloud). Symmetric NATs need Stage 3 (STUN/relay), which is deferred.

mTLS and access control

Transport is mTLS by default. The joiner trusts only the --tls-ca you provide — never system roots — so a public CA cannot MITM the mesh. Mint certs with the bundled CA helper:

tabbify-mesh-ca init --out ./mesh-ca
tabbify-mesh-ca issue-peer --name supervisor-01 --ca-dir ./mesh-ca --out ./certs

ACL policy is tag-based and enforced at register and heartbeat: a joiner only learns peers its tags are permitted to reach, and a policy PUT /v1/policy triggers an instant SSE resync. Join tokens are validated against auth & join tokens when AUTH_URL is set — and the token's claims, not the joiner's --tag flags, are authoritative.

One honest caveat: the joiner still defaults to insecure_no_mtls=true for backward compatibility with plaintext smoke tests. Loopback dev uses --insecure-no-mtls against a plaintext coordinator (TABBIFY_ALLOW_INSECURE=1); production must supply real certs on both ends. Coordinator state is in-memory only — peers re-register within one heartbeat after a restart.

See self-hosting a node to join a machine to the mesh.