Protocol Reference
This document describes the tunnel protocol used between the Expose macOS app and server.
Connection
The tunnel uses a single WebSocket connection:
wss://api.{domain}/api/v1/tunnel/connectAll communication happens over this connection using a mix of JSON control messages and binary data frames.
Control Messages
Control messages are JSON text frames with adjacently-tagged format:
{ "type": "MessageType", "data": { ... }}Authentication
Auth (client → server)
{ "type": "Auth", "data": { "api_key": "exp_k1_..." }}AuthResult (server → client)
{ "type": "AuthResult", "data": { "success": true, "user_id": "uuid", "error": null }}Tunnel Registration
RegisterTunnel (client → server)
{ "type": "RegisterTunnel", "data": { "subdomain": "myapp", "local_port": 3000 }}TunnelReady (server → client)
{ "type": "TunnelReady", "data": { "subdomain": "myapp", "public_url": "https://myapp.expose.domain.com" }}Stream Lifecycle
StreamOpen (server → client)
{ "type": "StreamOpen", "data": { "stream_id": 12345, "method": "GET", "path": "/api/users", "headers": [["Host", "myapp.expose.domain.com"], ["Content-Type", "application/json"]] }}StreamResponse (client → server)
{ "type": "StreamResponse", "data": { "stream_id": 12345, "status": 200, "headers": [["Content-Type", "application/json"]] }}StreamClose (bidirectional)
{ "type": "StreamClose", "data": { "stream_id": 12345 }}StreamError (bidirectional)
{ "type": "StreamError", "data": { "stream_id": 12345, "error": "Connection refused" }}WebSocket Upgrade
WsUpgrade (server → client)
{ "type": "WsUpgrade", "data": { "stream_id": 12345, "path": "/ws", "headers": [...] }}WsUpgradeAccepted (client → server)
{ "type": "WsUpgradeAccepted", "data": { "stream_id": 12345 }}Keepalive
Heartbeat (bidirectional, every 30 seconds)
{ "type": "Heartbeat", "data": {}}Binary Frames
Binary frames carry request/response bodies and WebSocket data. Format:
┌─────────────┬──────────┬──────────────┬─────────────┐│ stream_id │ msg_type │ payload_len │ payload ││ (4 bytes) │ (1 byte) │ (4 bytes) │ (variable) │└─────────────┴──────────┴──────────────┴─────────────┘Message Types
| Type | Value | Description |
|---|---|---|
| RequestBody | 0x01 | HTTP request body chunk |
| ResponseBody | 0x02 | HTTP response body chunk |
| WsFrame | 0x03 | WebSocket frame data |
| EndOfBody | 0x04 | Signals end of body |
Stream IDs
- Assigned by server for incoming requests
- 32-bit unsigned integers
- Reused after stream closes
Flow Example
Complete request/response flow:
Server Client │ │ ├── StreamOpen (GET /api/users) ────►│ │ │ │ ├── Forward to localhost:3000 │ │ │◄── StreamResponse (200 OK) ────────┤ │◄── Binary (ResponseBody) ──────────┤ │◄── Binary (EndOfBody) ─────────────┤ │ │ ├── StreamClose ────────────────────►│Error Handling
Connection Errors
If the WebSocket connection drops:
- Client attempts reconnection
- Exponential backoff: 1s, 2s, 4s, 8s, …
- Maximum 10 attempts
Stream Errors
If a local request fails:
- Client sends StreamError
- Server returns 502 Bad Gateway to visitor
Protocol Errors
Invalid messages result in:
- Error logged
- Connection may be closed for severe errors