WebSocket Relay

The WebSocket relay middleware upgrades HTTP connections to WebSocket and relays messages to a target process.

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
The WebSocket connection is bound to the target process. If the process exits, the connection closes automatically.

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 process
process.monitor(websocket_pid)

-- Send message to WebSocket client from any process
process.send(websocket_pid, "ws.send", {type = "text", data = "hello"})
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:

process.send(websocket_pid, "ws.control", {
    target_pid = new_process_pid,
    message_topic = "ws.message"
})

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
If no origins configured, only same-origin requests are allowed.

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:header("X-WS-Relay", json.encode({
        target_pid = tostring(pid),
        message_topic = "ws.message",
        heartbeat_interval = "30s",
        metadata = {
            user_id = req:query("user_id")
        }
    }))
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 - Heartbeat frequency (e.g. 30s)
metadata object - Attached to all messages

Message Topics

The relay sends these messages to the target process:

Topic When Payload
ws.join Client connects client_pid, metadata
ws.message Client sends message client_pid, type, data, metadata
ws.heartbeat Periodic (if configured) client_pid, uptime, message_count
ws.leave Client disconnects client_pid, reason, metadata

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 data = msg:payload():data()

        if topic == "ws.join" then
            -- Client connected
            local client_pid = data.client_pid

        elseif topic == "ws.message" then
            -- Handle client message
            local content = json.decode(data.data)
            handle_message(data.client_pid, content)

        elseif topic == "ws.leave" then
            -- Client disconnected
            cleanup(data.client_pid)
        end
    end
end

Sending to Client

Send messages back using the client PID:

-- Send text message
process.send(client_pid, "ws.send", {
    type = "text",
    data = json.encode({event = "update", value = 42})
})

-- Send binary
process.send(client_pid, "ws.send", {
    type = "binary",
    data = binary_content
})

-- Close connection
process.send(client_pid, "ws.close", {
    code = 1000,
    reason = "Session ended"
})

Broadcasting

Track client PIDs to broadcast to multiple clients:

local clients = {}

-- On join
clients[client_pid] = true

-- On leave
clients[client_pid] = nil

-- Broadcast
local function broadcast(message)
    local data = json.encode(message)
    for pid, _ in pairs(clients) do
        process.send(pid, "ws.send", {type = "text", data = data})
    end
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