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.