Architecture overview
Tabbify is two planes bolted together. The substrate is an event-sourced core: an append-only log is the source of truth, and services react to events rather than calling each other. The app-layer is what turns your code into a running service — a CLI, a registry, a public node, supervisors with per-app runners, the private mesh that wires them, and an auth service that signs who gets in. This page is the map; each subsystem has its own page linked below.
The substrate core
The spine is the transaction-node: an append-only event log where every state
change — app_registered, app_started, secret_registered — is recorded
before it takes effect. Recovery is replay; every mutation is auditable. The
read-node builds projections off that log; the api-gateway terminates external
webhooks and resolves secrets out of band; the original sandbox-supervisor
subscribes to sandbox_spawn_requested and emits sandbox_log / sandbox_started
back into the log. Nothing talks to a service directly — inbound traffic becomes an
event, your app subscribes to a segment, and emits its result. See
Core concepts and Services & capabilities.
Two honest caveats: the event stream is fire-and-forget (a late subscriber misses events; recent ones sit in a ring buffer), and the chat approval card is designed but not yet wired in the frontend.
The app-layer
Six pieces take a tabbify.toml to a reachable URL:
- tcli —
tcli pushmints a UUID v7, stamps it into the manifest, and uploads artifacts to thetabbify-appsS3 bucket underapps/<uuid>/v<N>/. - registry — a mesh-only OCI registry (Zot behind a Rust wrapper) on
[ula]:5000, gated by tabbify auth, per-tenant namespaces. - supervisor + runner —
supervisordis pure control plane; it spawns one detachedtabbify-runnerper app on[app_ula]:8730and re-adopts living runners after a crash. - runtimes — each runner executes
wasm-http,docker, orfirecracker, chosen per[[deploy]]target, not baked at build. - mesh — a userspace WireGuard overlay; every app gets a deterministic IPv6
fd5a:1f02:<blake3(uuid)>::1. - node — the one public listener (
:8090), exposing REST + MCP and proxying/app/<uuid>over the mesh. - auth — Ed25519 JWTs and node-join tokens; the coordinator trusts token claims, not a peer's self-asserted
--tag.
The data path
A request from the public internet to a running app:
browser
│ HTTPS
▼
CloudFront (api.tabbify.io)
│
▼
tabbify-node :8090 Bearer-auth, derive_app_ula(uuid)
│ WireGuard tunnel (mesh)
▼
tabbify-runner [fd5a:1f02:…::1]:8730
│
▼
runtime (wasm-http | docker | firecracker)
The node derives fd5a:1f02:<blake3(uuid)[0:6]>::1 from the UUID and dials
[app_ula]:8730 directly — the supervisor computes the same address, so there's no
lookup table. Bodies stream both directions; an unreachable app is 502, a bad
UUID is 400. See Routing & public access.
Deploys flow the other way: git push → deploy pipeline →
supervisor spawns the runner → it returns app_ula. Start there with the
Quickstart, or run your own edge via
self-hosting a node.
Honest scope
App-ULA direct binding is staged — today the node still resolves the hosting
supervisor before dialing. The unified tabbify.toml is in flight: tcli mostly
reads the legacy manifest.toml, the supervisor parses tabbify.toml. The node's
Bearer key defaults to an RnD value, and its activation/version stores are
in-memory. NAT traversal handles public IPs and cone NATs; symmetric NATs (Stage 3
relay) are deferred.