Documentation
ReferenceArchitecture

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:

  • tclitcli push mints a UUID v7, stamps it into the manifest, and uploads artifacts to the tabbify-apps S3 bucket under apps/<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 + runnersupervisord is pure control plane; it spawns one detached tabbify-runner per app on [app_ula]:8730 and re-adopts living runners after a crash.
  • runtimes — each runner executes wasm-http, docker, or firecracker, 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 pushdeploy 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.