Channels
Public - Configure custom WebSocket endpoint paths and multiple channels
Custom WebSocket Paths
By default, Verani accepts WebSocket connections at /ws. You can customize this per room:
Important: Verani ONLY supports WebSocket connections. All non-WebSocket requests are rejected with clear error messages:
- HTTP 426 (Upgrade Required) for non-WebSocket requests
- HTTP 404 for wrong paths with the correct path information
import { defineRoom, createActorHandler } from "verani";
// Chat room at /chat
export const chatRoom = defineRoom({
name: "chat",
websocketPath: "/chat", // Custom path
onConnect(ctx) {
console.log(`User connected to /chat`);
},
onMessage(ctx, frame) {
// Handle messages
}
});
// Presence room at /presence
export const presenceRoom = defineRoom({
name: "presence",
websocketPath: "/presence", // Different path
onConnect(ctx) {
console.log(`User connected to /presence`);
}
});
// Create handlers
const ChatRoom = createActorHandler(chatRoom);
const PresenceRoom = createActorHandler(presenceRoom);
export { ChatRoom, PresenceRoom };
Worker routing:
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// Route to chat room
if (url.pathname.startsWith("/chat")) {
const stub = ChatRoom.get("chat-instance");
return stub.fetch(request);
}
// Route to presence room
if (url.pathname.startsWith("/presence")) {
const stub = PresenceRoom.get("presence-instance");
return stub.fetch(request);
}
return new Response("Not Found", { status: 404 });
}
};
Client:
// Connect to chat
const chatClient = new VeraniClient("wss://example.com/chat?userId=alice");
// Connect to presence
const presenceClient = new VeraniClient("wss://example.com/presence?userId=alice");
// Trying to connect with HTTP will get clear error:
// HTTP 426: "This endpoint only accepts WebSocket connections at /chat"
// Wrong path will get clear error:
// HTTP 404: "WebSocket endpoint is /chat, not /wrong-path"
Multiple Channels
Let users join different channels within the same room.
import { defineRoom } from "verani";
interface ChannelMeta extends ConnectionMeta {
subscribedChannels: Set<string>;
}
export const multiChannelRoom = defineRoom<ChannelMeta>({
extractMeta(req) {
const url = new URL(req.url);
return {
userId: url.searchParams.get("userId") || "anonymous",
clientId: crypto.randomUUID(),
channels: ["lobby"], // Start in lobby
subscribedChannels: new Set(["lobby"])
};
}
});
// Register event handlers (socket.io-like)
multiChannelRoom.on("channel.join", (ctx, data) => {
const { channel } = data;
if (!ctx.meta.channels.includes(channel)) {
ctx.meta.channels.push(channel);
ctx.meta.subscribedChannels.add(channel);
// Notify others in that channel using emit API
ctx.actor.emit.to(channel).emit("channel.userJoined", {
userId: ctx.meta.userId
});
}
});
multiChannelRoom.on("channel.leave", (ctx, data) => {
const { channel } = data;
const idx = ctx.meta.channels.indexOf(channel);
if (idx !== -1) {
ctx.meta.channels.splice(idx, 1);
ctx.meta.subscribedChannels.delete(channel);
// Notify others using emit API
ctx.actor.emit.to(channel).emit("channel.userLeft", {
userId: ctx.meta.userId
});
}
});
multiChannelRoom.on("channel.message", (ctx, data) => {
const { channel, text } = data;
// Verify user is in channel
if (ctx.meta.subscribedChannels.has(channel)) {
// Broadcast to channel using emit API
ctx.actor.emit.to(channel).emit("chat.message", {
channel,
from: ctx.meta.userId,
text
});
} else {
ctx.emit.emit("error", {
message: `Not subscribed to channel: ${channel}`
});
}
});
Client:
// Join a channel
client.emit("channel.join", { channel: "general" });
// Send to specific channel
function sendToChannel(channel, text) {
client.emit("channel.message", { channel, text });
}
// Receive channel messages
client.on("chat.message", ({ channel, from, text }) => {
addMessageToChannel(channel, from, text);
});
Related Examples
- Basic Chat - Simple chat room
- Presence - Track online users
- Authentication - Secure authentication