Bridge connectivity protocol
This document is the normative bridge wire contract. Application routes carried inside a proxied request are opaque to the bridge.
Endpoint roots
- Phoenix WebSocket:
/ws/websocket?type=serveror/ws/websocket?type=app - Health:
GET /health - HTTP bridge protocol:
/bridge/v1/*
Phoenix clients join server:<server_id> or app:<client_id>. The current
clients encode messages as JSON objects:
{
"topic": "app:<client_id>",
"event": "event_name",
"payload": {},
"ref": "caller-correlation-reference"
}
Replies use Phoenix phx_reply status/response envelopes. Server pushes have
no caller request reference. Phoenix heartbeat uses topic phoenix, event
heartbeat.
Server control channel
The server connects with type=server, joins server:<server_id>, and sends:
| Event | Required payload | Meaning |
|---|---|---|
register |
server_id, display_name, computer_name; optional location |
Make this control connection the active server registration. |
http_response |
request_id, status, headers, body |
Complete a small legacy response. |
http_response_init |
request_id, status, headers, body/chunk metadata |
Start a chunked legacy response. |
http_response_chunk |
request_id, seq, chunk |
Send one ordered legacy response text chunk. |
http_response_end |
request_id, final_seq |
Finish a chunked legacy response. |
http_response_v2_init |
request_id, status, headers |
Start an HTTP v1 proxy response; the body is uploaded separately. |
The bridge may push:
| Event | Payload | Meaning |
|---|---|---|
pairing_requested |
client_id, code, expires_at |
Display the six-digit code to the user. |
paired |
client_id, session_id |
A legacy ephemeral session was created. |
http_request |
request metadata plus text body |
Execute a small legacy request. |
http_request_init |
request and chunk metadata | Start receiving a legacy chunked request. |
http_request_chunk |
request_id, seq, chunk |
Receive one ordered legacy request text chunk. |
http_request_end |
request_id, final_seq |
Execute the assembled legacy request. |
http_request_v2 |
metadata and transfer paths/tokens | Execute an HTTP v1 proxy request without WebSocket body bytes. |
http_request_v2_cancel |
request_id, reason |
Cancel in-flight v2 server work. |
superseded |
server_id, reason: "new_connection" |
Stop because a newer registration owns the server ID. |
An http_request_v2 payload contains:
{
"type": "http_request_v2",
"request_id": "<uuid>",
"method": "POST",
"path": "/opaque/application/path?query=value",
"headers": {},
"request_body_present": true,
"request_body_path": "/bridge/v1/transfers/<request_id>/request-body",
"request_body_token": "<signed-token>",
"response_body_path": "/bridge/v1/transfers/<request_id>/response-body",
"response_body_token": "<signed-token>"
}
The path values may be resolved against an explicitly configured bridge HTTP origin. The tokens are sent only through the server control channel.
Legacy app channel
The app connects with type=app, joins app:<client_id>, and may send:
| Event | Required payload | Successful result |
|---|---|---|
list_servers |
none | Public server records. |
pair_request |
server_id |
Requested status and code expiry. |
pair_submit |
server_id, code |
Fresh session_id and signed pairing_token. |
resume_session |
pairing_token; optional expected server_id |
Fresh session_id. |
http_request |
request_id, session_id, method, path, headers, text body/null |
Full legacy response or chunked-response marker. |
http_request_init |
request/session metadata and optional body/chunk metadata | Accepted. |
http_request_chunk |
request_id, session_id, seq, chunk |
Accepted. |
http_request_end |
request_id, session_id, final_seq |
Full legacy response or chunked-response marker. |
Chunked responses are pushed as http_response_init,
http_response_chunk, and http_response_end before the final
phx_reply. Sequences begin at zero and must be contiguous. final_seq is the
last emitted sequence.
Legacy request and response bodies are UTF-8 strings. This path does not define a raw-binary WebSocket encoding.
HTTP v1 API
All bridge HTTP v1 responses set Cache-Control: no-store. The router exposes
CORS headers allowing the supported methods and Content-Type,
Authorization, X-Crystal-Bridge-Token, and
X-Crystal-Bridge-Body-Present.
Errors use:
{"error": "stable_reason"}
Capabilities
GET /bridge/v1/capabilities
No authentication. A compatible response identifies protocol version 1,
HTTP proxy support, body download/upload support, and http/hybrid modes.
An opt-in frontend fails explicitly if version 1 is not advertised.
Discovery
GET /bridge/v1/servers
No authentication. Returns:
{
"servers": [
{
"server_id": "<stable-id>",
"display_name": "Server",
"computer_name": "host",
"location": null
}
]
}
Internal channel PID, node, and connection timestamp are not public.
Pairing request
POST /bridge/v1/pairings/request
Body:
{"server_id": "<stable-id>", "client_id": "<frontend-id>"}
The response contains status, ISO-8601 expires_at, and an opaque
pairing_request_token. The response never contains the six-digit code.
Pairing confirmation
POST /bridge/v1/pairings/confirm
Body:
{"pairing_request_token": "<signed-handle>", "code": "123456"}
Success returns server_id and pairing_token. Representative errors include
expired request (410), unavailable owner (409), missing server (404),
and invalid/not-found pairing (400).
Pairing resume or validation
POST /bridge/v1/pairings/resume
Header:
X-Crystal-Bridge-Token: <pairing-token>
Body may contain the expected server_id. Success returns the token's
server_id and current public server record. This route does not create a
frontend WebSocket session.
Proxied application request
<supported-method> /bridge/v1/proxy/<opaque-path-and-query>
Header:
X-Crystal-Bridge-Token: <pairing-token>
X-Crystal-Bridge-Body-Present: 1 explicitly distinguishes a present
zero-length body from no body. Content length or transfer encoding also marks a
body as present.
The bridge strips its path prefix and forwards the remaining path/query to the
registered server. It preserves the caller's application Authorization
header. JSON, text, and binary body bytes are opaque to the bridge.
Before response headers, errors are normal JSON HTTP errors. After a streamed response starts, later errors terminate that response stream.
Request-body transfer
GET /bridge/v1/transfers/<request_id>/request-body
Header:
Authorization: Bearer <request-body-transfer-token>
The credential must match request ID, server ID, request_body direction, and
expiry. A successful response is an identity-encoded
application/octet-stream body. Only one consumer may attach.
Response-body transfer
PUT /bridge/v1/transfers/<request_id>/response-body
Header:
Authorization: Bearer <response-body-transfer-token>
The credential must match request ID, server ID, response_body direction,
and expiry. Only one uploader may attach. Success returns 204 after the
frontend acknowledges the final response chunk.
Header filtering
The bridge does not forward:
Host;X-Crystal-Bridge-Token;X-Crystal-Bridge-Body-Present;Content-LengthorTransfer-Encoding;Connection,Keep-Alive,Proxy-Authenticate,Proxy-Authorization,TE,Trailer, orUpgrade.
The Python server requests Accept-Encoding: identity from its local
application. It removes stale local Content-Length and Content-Encoding
before response metadata crosses the server WebSocket.
Compatibility
Adding HTTP v1 does not remove or reinterpret legacy app/server channel events. HTTP/hybrid clients negotiate through capabilities. There is no protocol-level fallback or replay from HTTP v1 to legacy WebSocket proxying.