Skip to main content

The Local API & Auth Model

TermFlow runs a small HTTP server on your machine so that agents and your own tools can drive terminals. This page explains where that server lives, how the WebSocket stream works, and — most importantly — exactly when a token is required and when it is not.

The API server

Inside every TermFlow app is a local API server built on Axum (Rust). It exposes a REST surface under /api, a WebSocket at /ws, and a couple of always-open health checks. This is the same server the built-in MCP sidecar talks to on your behalf, and the same one you'd target if you build your own automation.

By default it binds to 127.0.0.1 (loopback only), so nothing outside your machine can reach it.

Ports

InterfaceProd (installed app)Dev (tauri dev)What
API (REST + WebSocket)4203142051Axum HTTP + WebSocket
MCP sidecar4203242052Streamable-HTTP MCP bridge

The rest of these docs default to the prod ports (42031 / 42032). Both are user-overridable in Settings → Connections (any value in 1024–65535). If a port is already owned by another instance, the Connections panel shows a conflict state on its health dot.

The MCP server is a separate process

The MCP sidecar (port 42032) is a distinct process that bridges MCP clients to this API. You don't talk to the API directly when using an agent over MCP — see The MCP server. This page is about the underlying local API itself.

The WebSocket stream

The API server also serves a WebSocket for live terminal output and input:

ws://127.0.0.1:42031/ws        (alias: /api/ws)

On connect, the server sends a welcome frame, then streams terminal events:

{ "id": "welcome", "success": true, "data": { "version": "0.1.0", "mode": "tauri" } }

Server-to-client output frames look like {"type":"event","event":{"type":"output.data","terminalId":"…","data":{"content":"…"}}}; clients send heartbeat, subscribe, and command frames back. The full frame catalogue lives in the WebSocket reference.

The auth model

This is the part worth reading carefully, because TermFlow's default is deliberately open and that surprises people.

The one rule: TermFlow's auth gate enforces a token only when the server is exposed on the LAN. In the default localhost-only mode, every endpoint is unauthenticated — and that is safe because the server is bound to 127.0.0.1, where only processes on your own machine can reach it.

Here is the exact decision the gate makes on every request:

Two consequences worth internalizing:

  • On localhost, the token is optional. You can call GET /api/terminals with no Authorization header and it just works. Loopback binding is the security boundary here, not the token.
  • /health and /api/health are always open, regardless of mode — so monitoring never needs a credential.

The real credential: your auth token

When you do need to authenticate (because you've exposed the server — see below), the credential is the authToken: a 64-character hex string generated on first run and stored in TermFlow's config file. You'll find it in Settings → Connections, masked, with reveal / copy / Rotate controls.

Send it as a bearer token on REST calls:

GET /api/terminals HTTP/1.1
Host: 192.168.1.20:42031
Authorization: Bearer 0f3c…<64 hex chars>…a91b

Because browsers can't set custom headers on a WebSocket handshake, the WebSocket takes the token as a query parameter instead:

ws://192.168.1.20:42031/ws?token=0f3c…a91b
warning
Do not use POST /api/auth/token

That endpoint exists and returns a JWT — but the auth gate does not check that JWT. It is leftover monitor-compatibility scaffolding. Authenticating with it will not do what you expect. The credential the gate actually verifies is the authToken from Settings → Connections. Ignore /api/auth/token.

Exposing on the local network

If you want another device (a phone, a second laptop, a remote monitor) to reach TermFlow, turn on "Expose on local network" in Settings → Connections (default: off).

Flipping it on changes two things at once:

  1. Binding moves from 127.0.0.1 to 0.0.0.0 — the server now listens on every network interface, and the panel lists the reachable per-NIC IP addresses.
  2. The auth gate switches on. From this point, every request (except the health checks) must carry a valid bearer token, and the WebSocket must carry ?token=.

The Connections panel marks this mode with a ⚠ warning badge, because you are now reachable by anything on your LAN. Only requests presenting the correct authToken get through.

┌──────────────── Settings → Connections ─────────────────┐
│ API port [ 42031 ] MCP port [ 42032 ] │
│ │
│ [x] Expose on local network ⚠ reachable on LAN │
│ Reachable at: 192.168.1.20 , 10.0.0.5 │
│ │
│ Auth token •••••••••••••••••••••• [Reveal] [Copy] │
│ [ Rotate ] │
│ │
│ Health ● healthy [ Save & apply (restart) ] │
└──────────────────────────────────────────────────────────┘

Rotating the token

Hit Rotate in Settings → Connections to mint a fresh 64-hex token. Rotation takes effect without a server restart, and existing UI connections survive it — but any external client you've configured with the old token will need the new one. Rotate if a token leaks, or routinely as hygiene when you keep the server exposed.

An honest note: the localhost-open default is a real design choice, not an oversight — a local agent driving your terminal shouldn't have to juggle a secret to do so. It is safe only as long as the server stays bound to loopback. The moment you enable "Expose on local network," treat the authToken like any other credential: don't paste it into shared configs, rotate it if it leaks, and prefer keeping exposure off unless you genuinely need cross-device access.

Next steps