Relay Protocol Specification
Version: 1.0 Status: Implemented (Phase C.6) Audience: Developers implementing the relay tunnel (Step C.6)
Overview
The relay tunnel enables remote clients to reach a phlix-server instance that is behind a NAT or firewall without any port forwarding on the server side.
The server opens a persistent WSS connection to the hub's relay endpoint. The hub multiplexes inbound HTTP requests from remote clients over this tunnel and proxies responses back.
This is the same pattern as frp, ngrok, or cloudflared but implemented in PHP/Workerman.
Key distinction from heartbeat: The heartbeat connection is a periodic HTTPS POST call from the server to the hub (60-second interval). The relay tunnel is a persistent WebSocket connection initiated by the server to the hub, used to carry arbitrary HTTP traffic bidirectionally.
Architecture
Client (remote) Hub Server (behind NAT)
| | |
| HTTPS request to | |
| https://<id>.phlix.media/* | |
| ----------------------------> |
| | HTTP-over-WebSocket frame |
| | ---------------------------->
| | |
| | HTTP-over-WebSocket response|
| | <----------------------------
| HTTPS response | |
| <------------------------------ |Connection Lifecycle
Server initiates
- Server starts
RelayApplicationifPHLIX_RELAY_ENABLED=trueandhub-enrollment.jsonexists. RelayConsumeropens a WSS connection towss://hub.example.com/api/v1/servers/{server_id}/relay.- On connect, server sends a
REGISTERframe with its enrollment JWT. - Hub validates the JWT and associates the WebSocket connection with the server's
relay_sessionDB record.
Normal operation
- Hub receives an inbound HTTPS request for
https://<id>.phlix.media/api/v1/relay/.... - Hub routes the request to the correct server via the persistent WSS connection.
- Server receives the
HTTP_REQUESTframe, dispatches it locally, and returns anHTTP_RESPONSEframe. - Hub proxies the response back to the client.
Keep-alive
- Server sends a
PINGframe everyPHLIX_RELAY_PING_INTERVALseconds (default 30). - Hub responds with a
PONGframe. - If no
PONGis received withinPHLIX_RELAY_PING_TIMEOUT(default 10), the connection is considered dead. - Server auto-reconnects after
PHLIX_RELAY_RECONNECT_DELAYseconds (default 5).
Wire Format
All frames share the same binary structure:
[1-byte type][4-byte seq (big-endian uint32)][4-byte payload_len (big-endian uint32)][payload_bytes]Frame types
| Constant | Value | Direction | Description |
|---|---|---|---|
TYPE_HTTP_REQUEST | 1 | Hub → Server | HTTP request proxied to the server |
TYPE_HTTP_RESPONSE | 2 | Server → Hub | HTTP response from the server |
TYPE_PING | 3 | Either → Either | Keep-alive probe |
TYPE_PONG | 4 | Either → Either | Keep-alive acknowledgement |
HTTP Request payload (JSON)
{
"seq": 42,
"method": "GET",
"path": "/api/v1/libraries",
"headers": {
"Authorization": "Bearer ...",
"Accept": "application/json"
},
"body": ""
}HTTP Response payload (JSON)
{
"seq": 42,
"status": 200,
"headers": {
"Content-Type": "application/json",
"Content-Length": "27"
},
"body": "{\"media_items\":[]}"
}PING / PONG payload (JSON)
{"seq": 7}REGISTER Frame (initial)
On connect, the server sends a TYPE_HTTP_REQUEST frame where method = "REGISTER":
{
"seq": 1,
"method": "REGISTER",
"path": "/relay/register",
"headers": {
"Authorization": "Bearer <enrollment_jwt>",
"X-Server-Id": "<server_uuid>"
},
"body": "{\"server_id\":\"<server_uuid>\"}"
}Hub responds with a TYPE_HTTP_RESPONSE with status = 200 on success, 401 if the JWT is invalid.
Server-side Components
Phlix\Hub\RelayMessageFramer
Frames and parses binary relay messages.
$framer = new RelayMessageFramer();
$bytes = $framer->frameRequest($seq, 'GET', '/api/v1/libraries', $headers, '');
$frame = $framer->parse($bytes); // => RelayFrame|nullPhlix\Hub\RelayConsumer
Maintains the WSS connection to the hub. Receives frames, dispatches locally via Router, and sends responses.
$consumer = new RelayConsumer($config, $hubClient, $logger, $serverId);
$consumer->start(); // opens WSS connection
$consumer->stop(); // graceful shutdown
$consumer->isConnected();Phlix\Hub\RelayApplication
Workerman Worker wrapper providing the timer context.
Phlix\Hub\RelayConfig
Configuration from config/relay.php / environment variables.
Error Handling
| Scenario | Behavior |
|---|---|
| Connection drop | RelayConsumer schedules a reconnect after PHLIX_RELAY_RECONNECT_DELAY |
| Hub returns non-2xx on REGISTER | Connection closed, no reconnect |
| Local router throws | RelayConsumer returns HTTP 500 over the tunnel |
| Incomplete binary frame | Buffer retained until more data arrives |
| Unknown frame type | Logged and discarded |
Database
Hub-side relay_sessions table tracks:
id— UUID of the relay sessionserver_id— FK toservers.idconnected_at— Unix timestamplast_frame_at— Unix timestamp of last activitybytes_sent— total bytes sent to serverbytes_received— total bytes received from server
Related Documents
docs/dev/pairing-protocol.md— Pairing and enrollment protocolconfig/relay.php— Configuration referencedocs/reference/env-vars.md— Environment variable reference