WebSocket Handlers

Build real-time applications with WebSocket support, channel pub/sub, and presence tracking.

Overview

Lua scripts can act as WebSocket handlers by registering with the WS HTTP method. When a client connects via WebSocket, your script runs for the duration of the connection, enabling:

Basic Messaging

Send and receive messages between server and client.

Channel Pub/Sub

Subscribe to channels and broadcast to all subscribers.

Presence Tracking

Track who's connected to each channel in real-time.

Basic WebSocket Operations

Core functions for WebSocket communication via solidb.ws.

  • solidb.ws.send(data) -> nil
    Send a message (string) to the connected client.
  • solidb.ws.recv() -> string | nil
    Wait for and receive a message from the client. Returns nil if connection closes.
  • solidb.ws.recv_any(timeout_ms) -> (msg, type) | nil
    Wait for WebSocket messages OR channel/presence events. Returns message and type ("ws", "channel", "presence").
  • solidb.ws.close() -> nil
    Close the WebSocket connection gracefully.
Simple Echo Server
-- Echo server: returns every message back to the client
while true do
    local msg = solidb.ws.recv()
    if msg == nil then break end  -- Connection closed

    solidb.ws.send(json.encode({
        type = "echo",
        message = msg,
        timestamp = os.time() * 1000
    }))
end

Channel Pub/Sub

Channels allow broadcast messaging between WebSocket connections. Any subscriber can broadcast, and all subscribers receive the message.

  • solidb.ws.channel.subscribe(name) -> true
    Subscribe to a channel to receive broadcast messages.
  • solidb.ws.channel.unsubscribe(name) -> true
    Unsubscribe from a channel.
  • solidb.ws.channel.broadcast(name, data) -> true
    Send data to all subscribers of a channel (including yourself).
  • solidb.ws.channel.list() -> table
    Get list of channels this connection is subscribed to.
Multi-Channel Notifications
-- Subscribe to multiple notification channels
local user_id = request.query.user_id

solidb.ws.channel.subscribe("user:" .. user_id)
solidb.ws.channel.subscribe("global:announcements")

while true do
    local msg, msg_type = solidb.ws.recv_any(30000)
    if msg == nil then break end

    if msg_type == "channel" then
        -- Forward broadcast to client
        solidb.ws.send(json.encode({
            type = "notification",
            channel = msg.channel,
            data = msg.data
        }))
    end
end

Presence Tracking

Track which users are connected to each channel. When users join or leave, all subscribers receive presence events.

  • solidb.ws.presence.join(channel, user_info) -> true
    Join a channel's presence list with user metadata. Other subscribers get a "join" event.
  • solidb.ws.presence.leave(channel) -> true
    Leave a channel's presence list. Other subscribers get a "leave" event.
  • solidb.ws.presence.list(channel) -> table
    Get all users currently in a channel. Returns array of {connection_id, user_info, joined_at}.
Who's Online Feature
local room = request.query.room or "lobby"
local user_info = {
    id = request.user.username,
    name = request.query.name,
    avatar = request.query.avatar
}

-- Join presence and subscribe to the room
solidb.ws.presence.join(room, user_info)
solidb.ws.channel.subscribe(room)

-- Send current user list
solidb.ws.send(json.encode({
    type = "init",
    users = solidb.ws.presence.list(room)
}))

while true do
    local msg, msg_type = solidb.ws.recv_any()
    if msg == nil then break end

    if msg_type == "presence" then
        -- Someone joined or left
        solidb.ws.send(json.encode({
            type = "user_" .. msg.event_type,
            user = msg.user_info
        }))
    end
end

Event Types from recv_any()

When using recv_any(), you receive different event types:

msg_type == "ws"

Direct message from the connected client (string).

local data = json.decode(msg)

msg_type == "channel"

Broadcast message from another connection (table).

msg.channel, msg.data, msg.timestamp, msg.sender_id

msg_type == "presence"

User joined or left a channel (table).

msg.event_type ("join"/"leave"), msg.channel, msg.user_info, msg.connection_id

Complete Example: Chat Room

A full chat room implementation with presence tracking.

chat_room.lua
-- Register: POST /_api/custom/_system/chat with method="WS"
-- Connect: ws://localhost:6745/api/custom/_system/chat?room=general&name=Alice

local room = request.query.room or "general"
local channel = "chat:" .. room
local user = {
    name = request.query.name or "Anonymous",
    joined = os.time() * 1000
}

-- Join the room
solidb.ws.channel.subscribe(channel)
solidb.ws.presence.join(channel, user)

-- Send current users to new client
solidb.ws.send(json.encode({
    type = "init",
    room = room,
    users = solidb.ws.presence.list(channel)
}))

-- Message loop
while true do
    local msg, msg_type = solidb.ws.recv_any(30000)
    if msg == nil then break end

    if msg_type == "ws" then
        local data = json.decode(msg)
        if data.type == "message" then
            solidb.ws.channel.broadcast(channel, {
                type = "chat",
                from = user.name,
                text = data.text,
                timestamp = os.time() * 1000
            })
        end
    elseif msg_type == "channel" then
        solidb.ws.send(json.encode(msg.data))
    elseif msg_type == "presence" then
        solidb.ws.send(json.encode({
            type = "user_" .. msg.event_type,
            user = msg.user_info
        }))
    end
end
-- Cleanup is automatic when connection closes

Client Integration

Connect from the browser or any WebSocket client to your Lua handler.

JavaScript Client
// Connect to WebSocket handler
const ws = new WebSocket(
  "ws://localhost:6745/api/custom/_system/chat?room=general&name=Alice"
);

ws.onopen = () => {
  console.log("Connected!");
};

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);

  if (msg.type === "init") {
    console.log("Users online:", msg.users);
  } else if (msg.type === "chat") {
    console.log(`${msg.from}: ${msg.text}`);
  } else if (msg.type === "user_join") {
    console.log(`${msg.user.name} joined`);
  } else if (msg.type === "user_leave") {
    console.log(`${msg.user.name} left`);
  }
};

// Send a chat message
function sendMessage(text) {
  ws.send(JSON.stringify({ type: "message", text }));
}

Best Practices

  • 1.
    Use recv_any() for real-time apps - It handles client messages and server events in one call.
  • 2.
    Structure channel names - Use prefixes like chat:, user:, team: for organization.
  • 3.
    Keep presence data minimal - Only include essential user info to reduce memory usage.
  • 4.
    Handle nil gracefully - A nil return from recv/recv_any means connection closed or timeout.
  • 5.
    Cleanup is automatic - When connection closes, presence leaves and subscriptions are removed automatically.