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
| Interface | Prod (installed app) | Dev (tauri dev) | What |
|---|---|---|---|
| API (REST + WebSocket) | 42031 | 42051 | Axum HTTP + WebSocket |
| MCP sidecar | 42032 | 42052 | Streamable-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 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/terminalswith noAuthorizationheader and it just works. Loopback binding is the security boundary here, not the token. /healthand/api/healthare 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
POST /api/auth/tokenThat 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:
- Binding moves from
127.0.0.1to0.0.0.0— the server now listens on every network interface, and the panel lists the reachable per-NIC IP addresses. - 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
authTokenlike 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
- Settings → Connections & Network — where ports, exposure, and the token live.
- API reference: Overview & auth — the full endpoint surface and header details.