Architecture¶
Processes¶
- Control plane (
server/, Fastify) — portal/REST API on:8080. It also runs the data plane: HTTP proxy (:5999), database proxy (:5432/:3306/:6379), the runner tunnel WebSocket (:8083), and the web terminal (on the main port). - Runner (
runner/) — one per computer. It dials out to the tunnel. It runs workloads via a container runtime (nerdctl/docker/podman,ALTHOST_CONTAINER_RUNTIME).
Request path (hosted apps)¶
client → <slug>.althostproxy.com → HTTP proxy (:5999)
→ look up route (slug → runner + local port)
→ forward over that runner's tunnel socket
→ runner → 127.0.0.1:<local port> (the app) → response back the same way
The runner advertises its live apps ({slug → port}) over the tunnel. The
control plane builds the routing table from those advertisements.
Database path¶
External DB clients connect to the control plane's standard engine port. The db-proxy resolves the target by database name + credentials. It relays the raw TCP stream over the owning runner's tunnel to the engine container on the host.
Tunnel authentication¶
The runner authenticates the tunnel WebSocket with its runner token
(Authorization: Bearer runner:<token>) on connect. The control plane validates
the token against the stored hash. It binds the socket to that node. It
rejects unauthenticated or mismatched connects. Each token shows once. You can
rotate it.
Auth surfaces¶
| Surface | Auth |
|---|---|
| Portal / management API | Session JWT |
| Runner HTTP (heartbeat, config, sync, logs) | Runner token (hashed) |
| Runner tunnel (WS) | Runner token on connect |
Deploy API (/v1/deploy) |
API key + scope |
| Web terminal (WS) | JWT + per-node ownership |
WebSocket & streaming passthrough¶
Client WebSocket connections to hosted apps work. The proxy handles the HTTP
Upgrade. It then relays the raw socket to the app over the tunnel as a
bidirectional byte stream. The runner opens a plain TCP connection to the app
and pipes both ways, with pause/resume backpressure. Because the data is raw
bytes, the same path also carries SSE, chunked/streaming responses, and
arbitrary TCP. It runs entirely over the existing outbound tunnel, so NAT'd
runners keep working.
Plain HTTP request/response still uses the simpler buffered path. Only Upgrade
requests take the raw-stream path.
Tunnel framing & encoding¶
Data on the tunnel uses two binary frame kinds alongside plain JSON control frames:
- data
[0x01][streamId][payload]— raw stream chunks (WebSocket/SSE/TCP/DB). - envelope
[0x02][headerLen][JSON header][body]— a message with a body (HTTP request/response, backup chunks): metadata + raw body in one frame.
This design avoids the ~33% base64 inflation and per-chunk JSON work that the
earlier design paid. ALTHOST_TUNNEL_BINARY (default on) selects what each side
emits. Set it to 0 to fall back to the legacy base64-in-JSON encoding.
Decoders always accept both, so the control plane and runner never need to
agree on the setting. Rollback is safe. You can flip one side at a time.