bun-websocket-server — Claude Code skill for WebSocket in Bun
A skill packed with Bun WebSocket server patterns: handlers, pub/sub, broadcasting. Handy when you need a chat or real-time streaming without extra libraries.
npx skills add secondsky/claude-skills --skill bun-websocket-server -a claude-codeWhat bun-websocket-server does
The skill covers the WebSocket support built into Bun.serve(). It ships TypeScript snippets for request upgrade, open, message, close, drain, ping, and pong handlers. There's also a section on sending text, JSON, and binary via ws.send(), with compression and buffer checks.
Pub/sub is the main pattern: clients subscribe with ws.subscribe(), messages go out through ws.publish() or server.publish(). The skill walks through chat rooms, broadcast to all clients, attaching user data during upgrade, and token authentication before the upgrade happens.
Server options include maxPayloadLength, idleTimeout, backpressureLimit, perMessageDeflate, and sendPings. A table covers common errors like Upgrade failed and Message too large. The source is truncated, so treat this as a pattern collection rather than full Bun documentation.
What it helps with
- Run a WebSocket server on Bun with no third-party packages
- Build chat rooms with subscribe/publish
- Broadcast a message to every connected client
- Attach user data to a WebSocket connection
- Reject unauthorized upgrades before they happen
Example prompts for the agent
- Set up a Bun WebSocket server that echoes messages back
- Add pub/sub rooms: client sends a room name, messages stay in that room
- Show me how to send binary data with ws.send
How to install bun-websocket-server
1Skills CLI (Vercel)
npx skills add secondsky/claude-skills --skill bun-websocket-server -a claude-code2Manually: put the files in place
~/.claude/skills/bun-websocket-server/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/bun-websocket-server/
1Skills CLI (Vercel)
npx skills add secondsky/claude-skills --skill bun-websocket-server -a codex2Manually: put the files in place
.agents/skills/bun-websocket-server/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add secondsky/claude-skills --skill bun-websocket-server -a cursor2Manually: put the files in place
.cursor/skills/bun-websocket-server/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install secondsky/claude-skills bun-websocket-server2Manually: put the files in place
.github/skills/bun-websocket-server/SKILL.md1Skills CLI (Vercel)
npx skills add secondsky/claude-skills --skill bun-websocket-server -a gemini-cli2Platform CLI
gemini skills install https://github.com/secondsky/claude-skills --path plugins/bun/skills/bun-websocket-serverManually: put the files in place
.devin/skills/bun-websocket-server/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add secondsky/claude-skills --skill bun-websocket-server -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about bun-websocket-server
How do I install bun-websocket-server in Claude Code?
Run npx skills add secondsky/claude-skills --skill bun-websocket-server -a claude-code. The skill lands in ~/.claude/skills/bun-websocket-server/SKILL.md.
What does bun-websocket-server do?
It provides ready-made snippets for a Bun WebSocket server: request upgrade, connection handlers, text and binary sending, topic-based pub/sub, broadcasting, and authentication.
Do I need a separate WebSocket library with Bun?
No. The skill builds on the WebSocket support already inside Bun.serve(), so no extra packages are involved.
Source text
SKILL.md · MIT
---
name: bun-websocket-server
description: This skill should be used when the user asks about "WebSocket in Bun", "real-time communication", "Bun.serve websocket", "ws server", "socket connections", "pub/sub", "broadcasting messages", "WebSocket upgrade", or building real-time applications with Bun.
metadata:
version: "1.0.0"
license: MIT
---
# Bun WebSocket Server
Bun has built-in WebSocket support integrated with `Bun.serve()`.
## Quick Start
```typescript
const server = Bun.serve({
fetch(req, server) {
// Upgrade to WebSocket
if (server.upgrade(req)) {
return; // Upgraded successfully
}
return new Response("Not a WebSocket request", { status: 400 });
},
websocket: {
open(ws) {
console.log("Client connected");
},
message(ws, message) {
console.log("Received:", message);
ws.send(`Echo: ${message}`);
},
close(ws) {
console.log("Client disconnected");
},
},
});
console.log(`WebSocket server running on ws://localhost:${server.port}`);
```
## WebSocket Handlers
```typescript
Bun.serve({
fetch(req, server) {
server.upgrade(req);
},
websocket: {
// Client connected
open(ws) {
console.log("New connection");
},
// Message received
message(ws, message) {
// message is string | Buffer
if (typeof message === "string") {
console.log("Text:", message);
} else {
console.log("Binary:", message);
}
},
// Connection closed
close(ws, code, reason) {
console.log(`Closed: ${code} - ${reason}`);
},
// Drain event (buffer flushed)
drain(ws) {
console.log("Buffer drained");
},
// Ping received
ping(ws, data) {
// Pong sent automatically
},
// Pong received
pong(ws, data) {
console.log("Pong received");
},
},
});
```
## Sending Messages
```typescript
websocket: {
message(ws, message) {
// Send text
ws.send("Hello");
// Send JSON
ws.send(JSON.stringify({ type: "greeting", data: "Hello" }));
// Send binary
ws.send(new Uint8Array([1, 2, 3]));
ws.send(Buffer.from("binary data"));
// Send with compression
ws.send("compressed message", true);
// Check if buffer is full
const bufferedAmount = ws.send("data");
if (bufferedAmount > 1024 * 1024) {
console.log("Buffer getting full");
}
},
}
```
## Attaching Data to Connections
```typescript
interface UserData {
id: string;
name: string;
joinedAt: Date;
}
Bun.serve<UserData>({
fetch(req, server) {
const url = new URL(req.url);
const userId = url.searchParams.get("userId");
// Attach data during upgrade
server.upgrade(req, {
data: {
id: userId,
name: "User " + userId,
joinedAt: new Date(),
},
});
},
websocket: {
open(ws) {
// Access attached data
console.log(`${ws.data.name} connected`);
},
message(ws, message) {
console.log(`${ws.data.name}: ${message}`);
},
},
});
```
## Pub/Sub (Topics)
```typescript
Bun.serve({
fetch(req, server) {
const url = new URL(req.url);
const room = url.searchParams.get("room") || "general";
server.upgrade(req, {
data: { room },
});
},
websocket: {
open(ws) {
// Subscribe to a topic
ws.subscribe(ws.data.room);
// Publish to topic (excludes sender)
ws.publish(ws.data.room, `User joined ${ws.data.room}`);
},
message(ws, message) {
// Broadcast to all in room (excludes sender)
ws.publish(ws.data.room, message);
},
close(ws) {
// Unsubscribe (automatic on close)
ws.unsubscribe(ws.data.room);
ws.publish(ws.data.room, "User left");
},
},
});
```
## Broadcasting to All Clients
```typescript
Bun.serve({
fetch(req, server) {
server.upgrade(req);
},
websocket: {
open(ws) {
// Subscribe to global topic
ws.subscribe("global");
},
message(ws, message) {
// Broadcast to ALL clients including sender
server.publish("global", message);
},
},
});
```
## Server-Level Publish
```typescript
const server = Bun.serve({
fetch(req, server) {
const url = new URL(req.url);
// HTTP endpoint to publish
if (url.pathname === "/broadcast") {
const message = url.searchParams.get("msg");
server.publish("global", message);
return new Response("Broadcasted");
}
server.upgrade(req);
},
websocket: {
open(ws) {
ws.subscribe("global");
},
},
});
// Can also publish from outside fetch
setInterval(() => {
server.publish("global", `Server time: ${new Date().toISOString()}`);
}, 5000);
```
## WebSocket Options
```typescript
Bun.serve({
websocket: {
// Max message size (default 16MB)
maxPayloadLength: 1024 * 1024, // 1MB
// Idle timeout in seconds (default 120)
idleTimeout: 60,
// Backpressure limit
backpressureLimit: 1024 * 1024,
// Enable compression
perMessageDeflate: true,
// Or with options
perMessageDeflate: {
compress: "shared",
decompress: "shared",
},
// Send/receive pings
sendPings: true,
// Handlers
open(ws) {},
message(ws, message) {},
close(ws) {},
},
});
```
## Client-Side Connection
```javascript
// Browser
const ws = new WebSocket("ws://localhost:3000");
ws.onopen = () => {
ws.send("Hello Server!");
};
ws.onmessage = (event) => {
console.log("Received:", event.data);
};
ws.onclose = () => {
console.log("Disconnected");
};
```
## Authentication
```typescript
Bun.serve({
fetch(req, server) {
// Verify auth before upgrade
const token = req.headers.get("Authorization");
if (!verifyToken(token)) {
return new Response("Unauthorized", { status: 401 });
}
const user = decodeToken(token);
server.upgrade(req, {
data: { userId: user.id },
});
},
websocket: {
open(ws) {
console.log(`Authenticated user ${ws.data.userId} connected`);
},
},
});
```
## Common Errors
| Error | Cause | Fix |
|-------|-------|-----|
| `Upgrade failed` | Invalid request | Check upgrade headers |
| `Connection closed` | Client disconnect | Handle in close handler |
| `Message too large` | Exceeds maxPayloadLength | Increase limit or chunk data |
| `Backpressure` | Slow client | Check buffer, wait for drain |
## Common Patterns
### Chat Room
```typescript
Bun.serve({
fetch(req, server) {
const url = new URL(req.url);
const username = url.searchParams.get("user") || "Anonymous";
server.upgrade(req, {
data: { username },
});
},
websocket: {
open(ws) {
ws.subscribe("chat");
ws.publish("chat", `${ws.data.username} joined`);
},
message(ws, message) {
ws.publish("chat", `${ws.data.username}: ${message}`);
},
close(ws) {
ws.publish("chat", `${ws.data.username} left`);
},
},
});
```
## When to Load References
Load `references/compression.md` when:
- perMessageDeflate configuration
- Compression tuning
- Binary message handling
Load `references/scaling.md` when:
- Multiple server instances
- Redis pub/sub integration
- Horizontal scaling
© secondsky · MIT · secondsky/claude-skills