WebSocket Relay
WebSocket Relay
Section titled “WebSocket Relay”The WebSocket relay middleware upgrades HTTP connections to WebSocket and relays messages to a target process.
How It Works
Section titled “How It Works”- HTTP handler sets
X-WS-Relayheader with target process PID - Middleware upgrades connection to WebSocket
- Relay attaches to the target process and monitors it
- Messages flow bidirectionally between client and process
Process Semantics
Section titled “Process Semantics”WebSocket connections are full processes with their own PID. They integrate with the process system:
- Addressable → Any process can send messages to a WebSocket PID
- Monitorable → Processes can monitor WebSocket connections for exit events
- Linkable → WebSocket connections can be linked to other processes
- EXIT events → When connection closes, monitors receive exit notifications
-- Monitor a WebSocket connection from another processprocess.monitor(websocket_pid)
-- Send a message to the WebSocket client from any process.-- The relay wraps it as {topic, data} JSON; the topic name is arbitrary.process.send(websocket_pid, "update", "hello")Connection Transfer
Section titled “Connection Transfer”Connections can be transferred to a different process by sending a control message:
process.send(websocket_pid, "ws.control", { target_pid = new_process_pid, message_topic = "ws.message"})Configuration
Section titled “Configuration”Add as post-match middleware on a router:
- name: ws_router kind: http.router meta: server: gateway prefix: /ws post_middleware: - websocket_relay post_options: wsrelay.allowed.origins: "https://app.example.com"| Option | Description |
|---|---|
wsrelay.allowed.origins | Comma-separated allowed origins |
Handler Setup
Section titled “Handler Setup”The HTTP handler spawns a process and configures the relay:
local http = require("http")local json = require("json")
local function handler() local req = http.request() local res = http.response()
-- Spawn handler process local pid = process.spawn("app.ws:handler", "app:processes")
-- Configure relay res:set_header("X-WS-Relay", json.encode({ target_pid = tostring(pid), message_topic = "ws.message", heartbeat_interval = "30s", metadata = { user_id = req:query("user_id") } }))endRelay Config Fields
Section titled “Relay Config Fields”| Field | Type | Default | Description |
|---|---|---|---|
target_pid | string | required | Process PID to receive messages |
message_topic | string | ws.message | Topic for client messages |
heartbeat_interval | duration | 30s | Heartbeat frequency (e.g. 30s) |
metadata | object | - | Attached to all messages |
Message Topics
Section titled “Message Topics”The relay sends these messages to the target process:
| Topic | When | Payload |
|---|---|---|
ws.join | Client connects | JSON {client_pid, metadata} |
ws.message (or your message_topic) | Client sends message | Raw client payload (text frame → string, binary frame → bytes); the source PID of the relay package is the client PID |
ws.heartbeat | Periodic (every 30s by default; interval overridable via heartbeat_interval) | JSON {client_pid, uptime, message_count, metadata} |
ws.leave | Client disconnects | JSON {client_pid, metadata} |
Receiving Messages
Section titled “Receiving Messages”local json = require("json")
local function handler() local inbox = process.inbox()
while true do local msg, ok = inbox:receive() if not ok then break end
local topic = msg:topic() local from = msg:from() -- client connection PID
if topic == "ws.join" then -- Client connected — payload is {client_pid, metadata} local data = msg:payload():data() local client_pid = data.client_pid
elseif topic == "ws.message" then -- Raw client message; from() is the client PID local body = msg:payload():data() -- string or bytes handle_message(from, json.decode(body))
elseif topic == "ws.leave" then -- Client disconnected — payload is {client_pid, metadata} cleanup(from) end endendSending to Client
Section titled “Sending to Client”Send messages back using the client PID. Any topic you choose is wrapped as {topic, data} JSON and forwarded to the WebSocket. Every server-to-client message is sent as a single WebSocket TEXT frame containing the {topic, data} JSON wrapper. Binary payloads are base64-encoded into the data field; they are NOT sent as separate binary frames.
-- Send a structured message (any topic name)process.send(client_pid, "update", json.encode({event = "update", value = 42}))
-- Send binaryprocess.send(client_pid, "data", binary_content)
-- Close connection (payload is the close reason string)process.send(client_pid, "ws.close", "Session ended")The reserved topics from server → client are ws.control (relay reconfiguration) and ws.close (close the connection).
Broadcasting
Section titled “Broadcasting”Track client PIDs to broadcast to multiple clients:
local clients = {}
-- On joinclients[client_pid] = true
-- On leaveclients[client_pid] = nil
-- Broadcastlocal function broadcast(message) local data = json.encode(message) for pid, _ in pairs(clients) do process.send(pid, "broadcast", data) endendSee Also
Section titled “See Also”- Middleware - Middleware configuration
- Process - Process messaging
- WebSocket Client - Outbound WebSocket connections