# "WebSocket Relay" _Path: en/http/websocket-relay_ > "The WebSocket relay middleware upgrades HTTP connections to WebSocket and relays messages to a target process." ## Table of Contents - WebSocket Relay ## Content # WebSocket Relay The `websocket_relay` middleware upgrades an HTTP connection and relays WebSocket messages to a target process. **Classification: protocol reference with partial integration recipes.** The blocks assume an HTTP server, router, process host, target process, and security context. Application message handlers and client-state cleanup remain application-owned. ## How It Works 1. HTTP handler sets `X-WS-Relay` header with target process PID 2. Middleware upgrades connection to WebSocket 3. Relay attaches to the target process and monitors it 4. Messages flow bidirectionally between client and process ## 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 ```lua -- Monitor a WebSocket connection from another process local _, monitor_err = process.monitor(websocket_pid) if monitor_err then return nil, monitor_err end -- Send a message to the WebSocket client from any process. -- The relay wraps it as {topic, data} JSON; the topic name is arbitrary. local _, send_err = process.send(websocket_pid, "update", "hello") if send_err then return nil, send_err end ``` The relay monitors the target process. If the target exits, the WebSocket connection closes automatically and the client receives a close frame. ## Connection Transfer Connections can be transferred to a different process by sending a control message: ```lua local _, transfer_err = process.send(websocket_pid, "ws.control", { target_pid = new_process_pid, message_topic = "ws.message" }) if transfer_err then return nil, transfer_err end ``` ## Configuration Add as post-match middleware on a router: ```yaml - 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 | If no origins configured, only same-origin requests are allowed. ## Handler Setup The HTTP handler spawns a process and configures the relay: ```lua local http = require("http") local json = require("json") local function handler() local req, req_err = http.request() if req_err then return nil, req_err end local res, res_err = http.response() if res_err then return nil, res_err end local user_id, query_err = req:query("user_id") if query_err then return nil, query_err end -- Spawn handler process local pid, spawn_err = process.spawn("app.ws:handler", "app:processes") if spawn_err then return nil, spawn_err end -- Configure relay local relay_config, encode_err = json.encode({ target_pid = tostring(pid), message_topic = "ws.message", heartbeat_interval = "30s", metadata = { user_id = user_id } }) if encode_err then local _, terminate_err = process.terminate(pid) return nil, terminate_err or encode_err end local header_err = res:set_header("X-WS-Relay", relay_config) if header_err then local _, terminate_err = process.terminate(pid) return nil, terminate_err or header_err end end ``` ### 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 join/leave/heartbeat messages | ## 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 format, binary frame → Bytes format); `payload:data()` returns a Lua string for either format, and the source PID 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 ```lua 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, payload_err = msg:payload():data() if payload_err then return nil, payload_err end local client_pid = data.client_pid elseif topic == "ws.message" then -- Raw client message; from() is the client PID local incoming = msg:payload() local frame_format = incoming:get_format() -- "text/plain" or "application/octet-stream" local body, payload_err = incoming:data() -- Lua string in either case if payload_err then return nil, payload_err end -- Decode or dispatch `body` according to `frame_format` and the -- application's protocol. elseif topic == "ws.leave" then -- Client disconnected — payload is {client_pid, metadata} -- Release application state associated with `from`. end end end ``` ## 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 wrapper. Tables remain JSON objects in `data`; strings remain strings. Payloads that reach the relay in Bytes format are base64-encoded into `data`; they are not sent as separate binary frames. Lua `process.send` exports its arguments as Lua-format payloads, so a Lua string does not take the Bytes-format branch. ```lua -- Send a structured message (any topic name) process.send(client_pid, "update", json.encode({event = "update", value = 42})) -- Close connection (payload is the close reason string) local _, close_err = process.send(client_pid, "ws.close", "Session ended") if close_err then return nil, close_err end ``` The reserved topics from server → client are `ws.control` (relay reconfiguration) and `ws.close` (close the connection). ## Broadcasting Track client PIDs to broadcast to multiple clients: ```lua local clients = {} -- On join clients[client_pid] = true -- On leave clients[client_pid] = nil -- Broadcast local function broadcast(message) for pid, _ in pairs(clients) do local _, send_err = process.send(pid, "broadcast", message) if send_err then return nil, send_err end end return true end ``` For complex multi-room scenarios, spawn a separate handler process per room or use a central manager process that tracks room memberships. ## See Also - [Middleware](http/middleware.md) - Middleware configuration - [Process](lua/core/process.md) - Process messaging - [WebSocket Client](lua/http/websocket.md) - Outbound WebSocket connections ## Navigation Previous: "Static Files" (http/static) Next: "Server-Sent Events" (http/sse)