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.
-- 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.
-- 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}.
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.
-- 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.
// 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.