Skip to content

Examples

Real-world examples demonstrating common WebSocket patterns with Bun WebSocket Router.

Chat Application

A complete chat room implementation with authentication and message history.

typescript
import { z } from "zod";
import { WebSocketRouter, createMessageSchema } from "bun-ws-router/zod";
import { publish } from "bun-ws-router/zod/publish";

// Create factory
const { messageSchema, ErrorMessage } = createMessageSchema(z);

// Message schemas
const JoinRoomMessage = messageSchema("JOIN_ROOM", {
  roomId: z.uuid(),
  username: z.string().min(1).max(20),
});

const SendMessageMessage = messageSchema("SEND_MESSAGE", {
  roomId: z.uuid(),
  text: z.string().min(1).max(500),
});

const LeaveRoomMessage = messageSchema("LEAVE_ROOM", {
  roomId: z.uuid(),
});

const WelcomeMessage = messageSchema("WELCOME", {
  message: z.string(),
});

const UserJoinedMessage = messageSchema("USER_JOINED", {
  username: z.string(),
  userCount: z.number(),
});

const MessageSchema = messageSchema("MESSAGE", {
  username: z.string(),
  text: z.string(),
});

const UserLeftMessage = messageSchema("USER_LEFT", {
  username: z.string(),
  userCount: z.number(),
});

const UserDisconnectedMessage = messageSchema("USER_DISCONNECTED", {
  userCount: z.number(),
});

// Store active users per room
const rooms = new Map<string, Set<string>>();

// Create router
const router = new WebSocketRouter<{ username?: string }>()
  .onOpen((ctx) => {
    console.log(`Client ${ctx.ws.data.clientId} connected`);

    ctx.send(WelcomeMessage, {
      message: "Connected to chat server",
    });
  })

  .onMessage(JoinRoomMessage, (ctx) => {
    const { roomId, username } = ctx.payload;

    // Store username in custom data
    ctx.ws.data.username = username;

    // Create room if doesn't exist
    if (!rooms.has(roomId)) {
      rooms.set(roomId, new Set());
    }

    // Add user to room
    rooms.get(roomId)!.add(ctx.ws.data.clientId);

    // Subscribe to room updates
    ctx.ws.subscribe(`room:${roomId}`);

    // Notify room members
    publish(ctx.ws, `room:${roomId}`, UserJoinedMessage, {
      username,
      userCount: rooms.get(roomId)!.size,
    });
  })

  .onMessage(SendMessageMessage, (ctx) => {
    const { roomId, text } = ctx.payload;

    // Check if user is in room
    if (!rooms.get(roomId)?.has(ctx.ws.data.clientId)) {
      ctx.send(ErrorMessage, {
        code: "AUTHORIZATION_FAILED",
        message: "You must join the room first",
      });
      return;
    }

    // Broadcast message to room
    publish(ctx.ws, `room:${roomId}`, MessageSchema, {
      username: ctx.ws.data.username || "Anonymous",
      text,
    });
  })

  .onMessage(LeaveRoomMessage, (ctx) => {
    const { roomId } = ctx.payload;

    // Remove from room
    rooms.get(roomId)?.delete(ctx.ws.data.clientId);

    // Unsubscribe
    ctx.ws.unsubscribe(`room:${roomId}`);

    // Notify others
    publish(ctx.ws, `room:${roomId}`, UserLeftMessage, {
      username: ctx.ws.data.username || "Anonymous",
      userCount: rooms.get(roomId)?.size || 0,
    });
  })

  .onClose((ctx) => {
    // Clean up user from all rooms
    for (const [roomId, users] of rooms) {
      if (users.has(ctx.ws.data.clientId)) {
        users.delete(ctx.ws.data.clientId);

        publish(ctx.ws, `room:${roomId}`, UserDisconnectedMessage, {
          userCount: users.size,
        });
      }
    }
  });

// Start server
Bun.serve({
  port: 3000,
  fetch(req, server) {
    if (req.headers.get("upgrade") === "websocket") {
      return router.upgrade(req, { server });
    }
    return new Response("WebSocket server");
  },
  websocket: router.websocket,
});

Authentication & Authorization

Implementing JWT authentication with role-based access control.

typescript
import { z } from "zod";
import { WebSocketRouter, createMessageSchema } from "bun-ws-router/zod";
import jwt from "jsonwebtoken";

// Create factory
const { messageSchema, ErrorMessage, ErrorCode } = createMessageSchema(z);

// User roles
enum Role {
  USER = "user",
  ADMIN = "admin",
  MODERATOR = "moderator",
}

// Message schemas
const AuthMessage = messageSchema("AUTH", {
  token: z.string(),
});

const AdminActionMessage = messageSchema("ADMIN_ACTION", {
  action: z.enum(["kick", "ban", "mute"]),
  targetUserId: z.string(),
  reason: z.string().optional(),
});

const KickedMessage = messageSchema("KICKED", { reason: z.string() });
const MutedMessage = messageSchema("MUTED", { reason: z.string() });

// User data interface
interface UserData {
  userId: string;
  username: string;
  roles: Role[];
  authenticated: boolean;
}

// Create router
const router = new WebSocketRouter<UserData>()
  .onOpen((ctx) => {
    // Initialize as unauthenticated
    ctx.ws.data.userId = "";
    ctx.ws.data.username = "";
    ctx.ws.data.roles = [];
    ctx.ws.data.authenticated = false;

    // Give client time to authenticate
    setTimeout(() => {
      if (!ctx.ws.data.authenticated) {
        ctx.ws.close(1008, "Authentication required");
      }
    }, 5000);
  })

  .onMessage(AuthMessage, async (ctx) => {
    try {
      // Verify JWT token
      const decoded = jwt.verify(
        ctx.payload.token,
        process.env.JWT_SECRET!,
      ) as any;

      // Store user data in connection
      ctx.ws.data.userId = decoded.userId;
      ctx.ws.data.username = decoded.username;
      ctx.ws.data.roles = decoded.roles || [Role.USER];
      ctx.ws.data.authenticated = true;

      // Subscribe to user-specific channel
      ctx.ws.subscribe(`user:${decoded.userId}`);

      // Subscribe to role channels
      for (const role of decoded.roles) {
        ctx.ws.subscribe(`role:${role}`);
      }

      // Send success
      ctx.send({
        type: "AUTH_SUCCESS",
        payload: {
          userId: decoded.userId,
          username: decoded.username,
          roles: decoded.roles,
        },
      });
    } catch (error) {
      ctx.send(ErrorMessage, {
        code: "AUTHENTICATION_FAILED",
        message: "Invalid token",
      });

      // Close connection
      ctx.ws.close(1008, "Invalid token");
    }
  })

  .onMessage(AdminActionMessage, (ctx) => {
    // Check authentication
    if (!ctx.ws.data.authenticated) {
      ctx.send(ErrorMessage, {
        code: "AUTHENTICATION_FAILED",
        message: "Not authenticated",
      });
      return;
    }

    // Check authorization
    if (!ctx.ws.data.roles?.includes(Role.ADMIN)) {
      ctx.send(ErrorMessage, {
        code: "AUTHORIZATION_FAILED",
        message: "Admin access required",
      });
      return;
    }

    // Perform admin action
    const { action, targetUserId, reason } = ctx.payload;

    switch (action) {
      case "kick":
        // Send kick message to target user
        publish(ctx.ws, `user:${targetUserId}`, KickedMessage, {
          reason: reason || "No reason provided",
        });
        break;

      case "ban":
        // Add to ban list (implement your logic)
        console.log(`Banning user ${targetUserId}`);
        break;

      case "mute":
        // Send mute notification
        publish(ctx.ws, `user:${targetUserId}`, MutedMessage, {
          reason: reason || "No reason provided",
        });
        break;
    }
  });

Real-time Notifications

Push notifications system with topic subscriptions.

typescript
import { z } from "zod";
import { WebSocketRouter, createMessageSchema } from "bun-ws-router/zod";
import { publish } from "bun-ws-router/zod/publish";

const { messageSchema } = createMessageSchema(z);

// Notification types
enum NotificationType {
  INFO = "info",
  WARNING = "warning",
  ERROR = "error",
  SUCCESS = "success",
}

// Message schemas
const SubscribeMessage = messageSchema("SUBSCRIBE", {
  topics: z.array(z.string()).min(1),
});

const UnsubscribeMessage = messageSchema("UNSUBSCRIBE", {
  topics: z.array(z.string()).min(1),
});

const NotificationMessage = messageSchema("NOTIFICATION", {
  id: z.uuid(),
  type: z.nativeEnum(NotificationType),
  title: z.string(),
  message: z.string(),
  data: z.record(z.unknown()).optional(),
  timestamp: z.number(),
});

// Track subscriptions
const userSubscriptions = new Map<string, Set<string>>();

const router = new WebSocketRouter()
  .onOpen((ctx) => {
    // Initialize user subscriptions
    userSubscriptions.set(ctx.ws.data.clientId, new Set());

    // Subscribe to personal notifications
    ctx.ws.subscribe(`user:${ctx.ws.data.clientId}`);
  })

  .onMessage(SubscribeMessage, (ctx) => {
    const { topics } = ctx.payload;
    const subs = userSubscriptions.get(ctx.ws.data.clientId)!;

    // Subscribe to topics
    for (const topic of topics) {
      ctx.ws.subscribe(`topic:${topic}`);
      subs.add(topic);
    }
  })

  .onMessage(UnsubscribeMessage, (ctx) => {
    const { topics } = ctx.payload;
    const subs = userSubscriptions.get(ctx.ws.data.clientId)!;

    // Unsubscribe from topics
    for (const topic of topics) {
      ctx.ws.unsubscribe(`topic:${topic}`);
      subs.delete(topic);
    }
  })

  .onClose((ctx) => {
    // Clean up subscriptions
    userSubscriptions.delete(ctx.ws.data.clientId);
  });

// HTTP endpoint to send notifications
const server = Bun.serve({
  port: 3000,

  async fetch(req) {
    const url = new URL(req.url);

    // REST API to send notifications
    if (url.pathname === "/api/notify" && req.method === "POST") {
      const body = await req.json();

      const notification = {
        id: crypto.randomUUID(),
        type: body.type || NotificationType.INFO,
        title: body.title,
        message: body.message,
        data: body.data,
        timestamp: Date.now(),
      };

      // Broadcast to topic using server.publish
      // Note: publish() is for use within handlers with ctx.ws
      // For server-level broadcasting, use server.publish() directly
      if (body.topic) {
        server.publish(
          `topic:${body.topic}`,
          JSON.stringify({
            type: "NOTIFICATION",
            payload: notification,
            meta: { timestamp: Date.now() },
          }),
        );
      }

      // Send to specific user
      if (body.userId) {
        server.publish(
          `user:${body.userId}`,
          JSON.stringify({
            type: "NOTIFICATION",
            payload: notification,
            meta: { timestamp: Date.now() },
          }),
        );
      }

      return Response.json({ success: true, id: notification.id });
    }

    // WebSocket upgrade
    if (req.headers.get("upgrade") === "websocket") {
      return router.upgrade(req, { server });
    }

    return new Response("Notification Server");
  },

  websocket: router.websocket,
});

console.log("Notification server running on http://localhost:3000");

Rate Limiting

Implementing rate limiting to prevent spam.

typescript
import { z } from "zod";
import { WebSocketRouter, createMessageSchema } from "bun-ws-router/zod";
import { publish } from "bun-ws-router/zod/publish";

const { messageSchema, ErrorMessage } = createMessageSchema(z);

// Rate limiter class
class RateLimiter {
  private requests = new Map<string, number[]>();

  constructor(
    private maxRequests: number,
    private windowMs: number,
  ) {}

  check(clientId: string): boolean {
    const now = Date.now();
    const requests = this.requests.get(clientId) || [];

    // Remove old requests
    const validRequests = requests.filter((time) => now - time < this.windowMs);

    // Check limit
    if (validRequests.length >= this.maxRequests) {
      return false;
    }

    // Add current request
    validRequests.push(now);
    this.requests.set(clientId, validRequests);

    return true;
  }

  reset(clientId: string) {
    this.requests.delete(clientId);
  }
}

// Create rate limiters
const messageLimiter = new RateLimiter(10, 60000); // 10 per minute
const joinLimiter = new RateLimiter(5, 300000); // 5 per 5 minutes

// Message schema
const ChatMessage = messageSchema("CHAT_MESSAGE", {
  text: z.string().min(1).max(200),
});

const JoinChannelMessage = messageSchema("JOIN_CHANNEL", {
  channel: z.string(),
});

// Router with rate limiting
const router = new WebSocketRouter()
  .onMessage(ChatMessage, (ctx) => {
    // Check rate limit
    if (!messageLimiter.check(ctx.ws.data.clientId)) {
      ctx.send(ErrorMessage, {
        code: "RATE_LIMIT_EXCEEDED",
        message: "Too many messages. Please slow down.",
      });
      return;
    }

    // Process message
    publish(ctx.ws, "global", ChatMessage, ctx.payload);
  })

  .onMessage(JoinChannelMessage, (ctx) => {
    // Check join rate limit
    if (!joinLimiter.check(ctx.ws.data.clientId)) {
      ctx.send(ErrorMessage, {
        code: "RATE_LIMIT_EXCEEDED",
        message: "Too many join requests.",
      });
      return;
    }

    // Join channel
    ctx.ws.subscribe(ctx.payload.channel);
  })

  .onClose((ctx) => {
    // Clean up rate limit data
    messageLimiter.reset(ctx.ws.data.clientId);
    joinLimiter.reset(ctx.ws.data.clientId);
  });