Sockets

Type-safe real-time communication with Socket routes

Creating a Socket Route

Create a socket route in app/sockets/ by exporting handler functions:

// app/sockets/chat/route.ts import type { SocketUser, SocketContext, SocketMessage } from "@ademattos/bunbox"; export function onJoin(user: SocketUser, ctx: SocketContext) { console.log(`User ${user.id} joined`); ctx.broadcast("user-joined", { username: user.data.username }); } export function onMessage( user: SocketUser, message: SocketMessage, ctx: SocketContext ) { if (message.type === "chat-message") { ctx.broadcast("chat-message", { text: message.data.text, username: user.data.username, }); } } export function onLeave(user: SocketUser, ctx: SocketContext) { console.log(`User ${user.id} left`); ctx.broadcast("user-left", { username: user.data.username }); }

This creates a socket at /sockets/chat.

Defining a Protocol

Create a typed protocol for your messages:

// app/sockets/chat/protocol.ts import { defineProtocol } from "@ademattos/bunbox/client"; export const ChatProtocol = defineProtocol({ "chat-message": { text: "", username: "" }, "user-joined": { username: "" }, "user-left": { username: "" }, "typing": { isTyping: false, username: "" }, });

Client-Side Usage

Use the useSocket hook to connect to your socket:

import { useSocket } from "@ademattos/bunbox/client"; import { ChatProtocol } from "@/app/sockets/chat/protocol"; export default function Chat() { const { subscribe, publish, connected, username } = useSocket( "/sockets/chat", ChatProtocol, { username: "John" } ); // Subscribe to messages useEffect(() => { const unsubscribe = subscribe("chat-message", (msg) => { console.log(`${msg.data.username}: ${msg.data.text}`); }); return unsubscribe; }, [subscribe]); // Publish messages const sendMessage = (text: string) => { publish("chat-message", { text, username }); }; return ( <div> {connected ? "Connected" : "Connecting..."} <button onClick={() => sendMessage("Hello!")}>Send</button> </div> ); }

Authorization

Add authorization to socket connections:

export function onAuthorize( req: Request, userData: Record<string, string> ): boolean { // Validate user data before connection if (!userData.username || userData.username.length > 20) { return false; } return true; }

Socket Context

The context provides methods for communication:

export function onMessage( user: SocketUser, message: SocketMessage, ctx: SocketContext ) { // Broadcast to all connected users ctx.broadcast("message", { text: "Hello everyone" }); // Send to specific user ctx.sendTo(userId, "private", { text: "Hello!" }); // Get all connected users const users = ctx.getUsers(); console.log(`${users.length} users online`); }

Socket Message Structure

All socket messages have this structure:

interface SocketMessage<T = unknown> { type: string; // Message type from protocol data: T; // Message payload timestamp: number; // Server timestamp userId: string; // Sender's user ID }

User Data

Access user data in handlers:

export function onMessage(user: SocketUser, message: SocketMessage, ctx: SocketContext) { // User ID (auto-generated) console.log(user.id); // Custom user data from connection console.log(user.data.username); console.log(user.data.role); }

Complete Example

// app/sockets/chat/route.ts import type { SocketUser, SocketContext, SocketMessage } from "@ademattos/bunbox"; interface ChatData { text: string; username: string; } export function onAuthorize( req: Request, userData: Record<string, string> ): boolean { return Boolean(userData.username); } export function onJoin(user: SocketUser, ctx: SocketContext) { const username = user.data.username || user.id; console.log(`${username} joined chat`); ctx.broadcast("user-joined", { username }); console.log(`Total users: ${ctx.getUsers().length}`); } export function onMessage( user: SocketUser, message: SocketMessage, ctx: SocketContext ) { if (message.type === "chat-message") { const data = message.data as ChatData; ctx.broadcast("chat-message", { text: data.text, username: user.data.username || user.id, }); } } export function onLeave(user: SocketUser, ctx: SocketContext) { const username = user.data.username || user.id; ctx.broadcast("user-left", { username }); console.log(`${username} left chat`); }
// app/sockets/chat/protocol.ts import { defineProtocol } from "@ademattos/bunbox/client"; export const ChatProtocol = defineProtocol({ "chat-message": { text: "", username: "" }, "user-joined": { username: "" }, "user-left": { username: "" }, });
// app/chat/page.tsx import { useState, useEffect } from "react"; import { useSocket } from "@ademattos/bunbox/client"; import { ChatProtocol } from "@/app/sockets/chat/protocol"; export default function Chat() { const [messages, setMessages] = useState<string[]>([]); const [input, setInput] = useState(""); const { subscribe, publish, connected } = useSocket( "/sockets/chat", ChatProtocol, { username: "User" } ); useEffect(() => { const unsub1 = subscribe("chat-message", (msg) => { setMessages((prev) => [...prev, `${msg.data.username}: ${msg.data.text}`]); }); const unsub2 = subscribe("user-joined", (msg) => { setMessages((prev) => [...prev, `${msg.data.username} joined`]); }); return () => { unsub1(); unsub2(); }; }, [subscribe]); const sendMessage = () => { if (input.trim() && connected) { publish("chat-message", { text: input, username: "User" }); setInput(""); } }; return ( <div> <div> {messages.map((msg, i) => ( <div key={i}>{msg}</div> ))} </div> <input value={input} onChange={(e) => setInput(e.target.value)} onKeyPress={(e) => e.key === "Enter" && sendMessage()} disabled={!connected} /> <button onClick={sendMessage} disabled={!connected}> Send </button> </div> ); }

Type Safety

Sockets are fully type-safe:

// Server knows message types export function onMessage(user: SocketUser, message: SocketMessage, ctx: SocketContext) { if (message.type === "chat-message") { // TypeScript narrows the type const text: string = message.data.text; } } // Client has autocomplete publish("chat-message", { text: "Hi", username: "John" }); // ✓ publish("invalid", { foo: "bar" }); // ✗ Error

Use Cases

Chat Applications

export function onMessage(user: SocketUser, message: SocketMessage, ctx: SocketContext) { ctx.broadcast("chat-message", { text: message.data.text, username: user.data.username, }); }

Real-time Notifications

export function onMessage(user: SocketUser, message: SocketMessage, ctx: SocketContext) { if (message.type === "notify-all") { ctx.broadcast("notification", { title: message.data.title, body: message.data.body, }); } }

Multiplayer Games

export function onMessage(user: SocketUser, message: SocketMessage, ctx: SocketContext) { if (message.type === "player-move") { ctx.broadcast("game-update", { playerId: user.id, position: message.data.position, }); } }