WebSockets
Lucent provides built-in WebSocket support via a singleton wsManager and an Elysia plugin.
Setup
Mount LucentWebsocket.plugin() in your main entry point:
// src/index.ts
import { Elysia } from "elysia";
import { lucent, LucentWebsocket } from "@codesordinatestudio/lucent";
import config from "./lucent.config";
const app = new Elysia()
.use(await lucent(config))
.use(LucentWebsocket.plugin({ path: "/ws" }))
.listen(3000);
Usage in Hooks
Broadcast messages from anywhere — hooks, jobs, HTTP handlers — via the wsManager singleton:
// src/collections/Posts.ts
import { wsManager, defineCollection } from "@codesordinatestudio/lucent";
export const Posts = defineCollection({
slug: "posts",
fields: [
/* ... */
],
hooks: {
afterCreate: [
async ({ doc }) => {
wsManager.broadcastToRoom("posts", { type: "post:created", doc });
},
],
afterUpdate: [
async ({ doc }) => {
wsManager.broadcastToRoom("posts", { type: "post:updated", doc });
},
],
},
});
Manager API
wsManager is a singleton available throughout your app. You can also access it via LucentWebsocket.manager.
Rooms
| Method | Description |
|---|---|
joinRoom(id, room) | Add a connection to a room |
leaveRoom(id, room) | Remove a connection from a room |
getRoomMembers(room) | Returns an array of connection IDs in the room |
getConnectionRooms(id) | Returns an array of room names a connection is in |
listRooms() | Lists all active rooms |
Sending Messages
| Method | Description |
|---|---|
sendTo(id, payload) | Send a private message to a specific connection ID |
broadcastToRoom(room, payload, options?) | Send to everyone in a room (options.exclude?: string[]) |
broadcastAll(payload, options?) | Send to every connected client |
Metadata
| Method | Description |
|---|---|
setMeta(id, key, value) | Store arbitrary metadata on a connection |
getMeta(id, key) | Read metadata from a connection |
Stats
wsManager.getStats();
// { connections: 42, rooms: { posts: 12, chat: 30 }, heartbeatActive: true }
Client-Side Usage
const ws = new WebSocket("ws://localhost:3000/ws");
ws.onopen = () => {
// Join a room to receive targeted broadcasts
ws.send(JSON.stringify({ type: "join", room: "posts" }));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
switch (msg.type) {
case "ping":
// Acknowledge the server heartbeat — keeps the connection alive
ws.send(JSON.stringify({ type: "pong" }));
break;
case "post:created":
console.log("New post:", msg.doc);
break;
case "post:updated":
console.log("Updated post:", msg.doc);
break;
}
};
Supported Message Types
| Type | Payload | Description |
|---|---|---|
join | { room: "name" } | Join a room; server replies { type: "joined" } |
leave | { room: "name" } | Leave a room; server replies { type: "left" } |
broadcast | { room: "name", payload: { ... } } | Broadcast to room members (excluding sender) |
ping | — | Server replies { type: "pong" } |
pong | — | Acknowledge a server-side heartbeat ping |
Heartbeat
Lucent automatically keeps connections healthy with a server-side heartbeat. Every 30 seconds (by default) the server pings all active connections. Clients that do not respond within 10 seconds are evicted from all rooms and removed from the pool.
How it works
- Server sends
{ "type": "ping" }to every connected client. - Client replies with
{ "type": "pong" }(or the browser handles it natively via WS ping frames). - If no
pongis received within the timeout, the connection is silently evicted.
The heartbeat starts automatically when the first client connects. You do not need to configure anything.
Customising the heartbeat
// 60-second interval, 15-second response timeout
LucentWebsocket.plugin({
path: "/ws",
heartbeat: { interval: 60_000, timeout: 15_000 },
});
// Disable the heartbeat entirely (not recommended in production)
LucentWebsocket.plugin({
path: "/ws",
heartbeat: false,
});
Client-side pong handler
Always respond to server pings so your connections are not evicted:
const ws = new WebSocket("ws://localhost:3000/ws");
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "ping") {
// Acknowledge the heartbeat
ws.send(JSON.stringify({ type: "pong" }));
return;
}
// Handle your application messages
};
API
| Method | Description |
|---|---|
wsManager.startHeartbeat(ms?, ms?) | Start the heartbeat manually (auto-called on connect) |
wsManager.stopHeartbeat() | Stop the heartbeat interval |
wsManager.pong(id) | Mark a connection as alive (called automatically) |
wsManager.getStats() | Returns { connections, rooms, heartbeatActive } |
Per-Room Authorization
Use the onJoinRoom hook to control which connections may join a room. Return true to allow and false to deny. Async callbacks are fully supported.
app.use(
LucentWebsocket.plugin({
path: "/ws",
/**
* Called every time a client sends { type: "join", room: "..." }.
* Return false to send back { type: "error", message: "Access denied to room '...'" }
* and abort the join.
*
* ws.data contains the Elysia WS connection data — store your user/session
* in WS connection metadata (wsManager.setMeta) so you can read it here.
*/
onJoinRoom: async (room, ws) => {
// Example: only allow authenticated users to join "private-*" rooms
const userId = wsManager.getMeta(ws.id, "userId") as string | undefined;
if (room.startsWith("private-") && !userId) {
return false; // deny
}
return true; // allow
},
}),
);
Storing user identity on connect
Because the WS plugin runs outside Lucent's HTTP route auth pipeline, the simplest
way to authenticate a socket connection is to reuse resolveLucentUser() against
the handshake request and then store the resolved user in WS metadata:
import { Elysia } from "elysia";
import { LucentWebsocket, getLucent, resolveLucentUser, wsManager } from "@codesordinatestudio/lucent";
import config from "./lucent.config";
const lucent = await getLucent({ config });
app.ws(
"/ws",
LucentWebsocket.handler({
async onJoinRoom(room, ws) {
const user = await resolveLucentUser(ws.data.request, lucent);
if (user) {
wsManager.setMeta(ws.id, "userId", user.id);
wsManager.setMeta(ws.id, "role", user.role);
}
if (room.startsWith("private-") && !user) {
return false;
}
return true;
},
}),
);
This works with Lucent bearer tokens, Lucent JWT cookies, session cookies, and API keys.