@bytesocket/uws
v0.4.0
Published
High-performance WebSocket server for Node.js using uWebSockets.js
Downloads
2,179
Maintainers
Readme
@bytesocket/uws
High-performance WebSocket server for ByteSocket built on uWebSockets.js.
✅ Compatible with Ultimate Express and any framework exposing a
uWebSockets.jsinstance. ⚠️ Hyper Express supported only when accessing the underlying uWS instance.
Features
- Room-based pub/sub -- join/leave rooms, scoped middleware, bulk operations
- Authentication -- server-side auth function with callback and timeout
- Global middleware -- inspect or block any incoming message before processing
- Room middleware -- middleware chain per room+event, with
next()/next(err)flow - Lifecycle hooks -- upgrade, open, message, close, error -- all typed and cancellable
- Server-side broadcast -- emit to all sockets, a room, or multiple rooms
- Origin validation -- allowlist origins at the framework level
- Full TypeScript -- generic event maps shared with the client for end-to-end type safety
- Dual serialization -- JSON or binary MessagePack (
msgpackr) out of the box
Installation
# Server (Node.js backend)
npm install @bytesocket/uws
# or
pnpm add @bytesocket/uws
# or
yarn add @bytesocket/uws
# Client (browser / Node.js frontend)
npm install @bytesocket/client
# or
pnpm add @bytesocket/client
# or
yarn add @bytesocket/clientPeer dependency:
uWebSockets.jsmust be installed separately.
See uWebSockets.js installation for platform-specific instructions.
Quick Start
import uWS from "uWebSockets.js";
import { ByteSocket } from "@bytesocket/uws";
const app = uWS.App();
const io = new ByteSocket();
io.lifecycle.onOpen((socket) => {
console.log(`Socket ${socket.id} connected`);
socket.rooms.join("lobby");
});
io.lifecycle.onClose((socket, code) => {
console.log(`Socket ${socket.id} disconnected (${code})`);
});
io.on("hello", (socket, data) => {
socket.emit("welcome", { message: `Hello, ${data.name}!` });
});
io.attach(app, "/socket");
app.listen(3000, (token) => {
if (token) console.log("Listening on port 3000");
});Ultimate Express Compatibility
import express from "ultimate-express";
const app = express();
const io = new ByteSocket();
io.lifecycle.onOpen((socket) => {
console.log(`Socket ${socket.id} connected`);
socket.rooms.join("lobby");
});
io.lifecycle.onClose((socket, code) => {
console.log(`Socket ${socket.id} disconnected (${code})`);
});
io.on("hello", (socket, data) => {
socket.emit("welcome", { message: `Hello, ${data.name}!` });
});
io.attach(app.uwsApp, "/socket");
app.listen(3000, (token) => {
if (token) console.log("Listening on port 3000");
});Type-Safe Events
Share a single event interface between server and client for end-to-end type safety. You can use symmetric events (emit and listen share the same map) or asymmetric events (full control via interface extension).
Symmetric usage (most common)
Use SocketEvents<T> directly with a single event map:
import { ByteSocket, SocketEvents } from "@bytesocket/uws";
type MyEvents = SocketEvents<{
"chat:message": { text: string };
"user:joined": { userId: string };
}>;
const io = new ByteSocket<MyEvents>();
// Emit and listen share the same typed events
io.emit("chat:message", { text: "Server announcement" });
io.on("user:joined", (socket, data) => {
console.log(`User ${data.userId} joined`);
});
// Rooms also use the same map
io.rooms.emit("lobby", "chat:message", { text: "Welcome to the lobby" });
io.rooms.on("lobby", "chat:message", (socket, data, next) => {
console.log(`${socket.id} said: ${data.text}`);
next();
});Asymmetric usage (full control)
Extend SocketEvents and override specific properties to differentiate emit/listen/room maps:
import { ByteSocket, SocketEvents } from "@bytesocket/uws";
interface MyEvents extends SocketEvents {
emit: {
"server:broadcast": { text: string; from: string };
"room:created": { roomId: string };
};
listen: {
"user:message": { text: string };
"user:typing": { userId: string };
};
emitRoom: {
chat: { message: { text: string; sender: string } };
};
listenRoom: {
chat: { message: { text: string; sender: string } };
};
emitRooms: { rooms: ["lobby", "announcements"]; event: { alert: string } } | { rooms: ["roomA", "roomB"]; event: { message: { text: string } } };
}
const io = new ByteSocket<MyEvents>();
// Global emits/listens
io.emit("server:broadcast", { text: "Hello all", from: "system" });
io.on("user:message", (socket, data) => {
console.log(data.text); // string ✓
});
// Room-specific emits/listens (different maps per room)
io.rooms.emit("chat", "message", { text: "Hello!", sender: "server" });
io.rooms.on("chat", "message", (socket, data, next) => {
console.log(`${data.sender}: ${data.text}`);
next();
});All server methods (emit, on, off, once, rooms.emit, rooms.on, etc.) are fully typed -- wrong event names or payload shapes become compile-time errors.
Authentication
Validate credentials when a client first connects. Until auth succeeds, no user messages are processed.
import { ByteSocket } from "@bytesocket/uws";
interface MySocketData extends SocketData {
userId: number;
}
const io = new ByteSocket<MyEvents, MySocketData>({
auth: (socket, data, callback) => {
// data is whatever the client sent in its auth payload
if (data.token === "valid-token") {
callback({ userId: 42 }); // payload is attached to socket.payload
} else {
callback(null, new Error("Invalid token"));
}
},
authTimeout: 8000, // ms before closing unauthenticated connections
});
io.lifecycle.onOpen((socket) => {
// Only fires after successful auth
console.log("Authenticated user:", socket.payload);
});Rooms
Joining and leaving (server-side)
io.lifecycle.onOpen((socket) => {
socket.rooms.join("lobby");
socket.rooms.leave("lobby");
console.log(socket.rooms.list()); // ["__bytesocket_broadcast__"]
});Emitting to rooms
// From any socket instance
socket.rooms.emit("chat", "message", { text: "Hello room!", sender: "server" });
// From the server globally
io.rooms.emit("chat", "message", { text: "Announcement!", sender: "server" });
// Broadcast to all connected sockets
socket.broadcast("user:joined", { userId: socket.id, name: "Ahmed" });
io.emit("user:joined", { userId: "abc", name: "Ahmed" });Bulk operations
socket.rooms.bulk.join(["lobby", "notifications", "chat"]);
socket.rooms.bulk.leave(["lobby", "notifications"]);
socket.rooms.bulk.emit(["room1", "room2"], "alert", { msg: "Hello both!" });Room lifecycle hooks (join/leave guards)
// Single-room guard
io.rooms.lifecycle.onJoin((socket, room, next) => {
if (room === "admin" && !socket.payload?.isAdmin) {
// The error is sent to the client as a join_room_error with a proper ErrorContext
next(new Error("Not authorized"));
} else {
next();
}
});
// Bulk join guard
io.rooms.bulk.lifecycle.onJoin((socket, rooms, next) => {
console.log(`${socket.id} joining: ${rooms.join(", ")}`);
next();
});Room event middleware
// Middleware chain per room + event -- call next() to forward, next(err) to block
io.rooms.on("chat", "message", (socket, data, next) => {
if (data.text.includes("badword")) {
next(new Error("Profanity not allowed")); // message is not forwarded
} else {
next();
}
});
// One-time middleware
io.rooms.once("chat", "message", (socket, data, next) => {
console.log("First chat message ever:", data.text);
next();
});
// Remove middleware
io.rooms.off("chat", "message", myMiddleware);
io.rooms.off("chat", "message"); // remove all for this event
io.rooms.off("chat"); // remove all for this roomGlobal Middleware
Runs before any user message is dispatched to listeners.
// Synchronous
io.use((socket, ctx, next) => {
console.log("Incoming message:", ctx.event);
next();
});
// Async
io.use(async (socket, ctx, next) => {
await logToDatabase(socket.id, ctx);
next();
});
// Block a message
io.use((socket, ctx, next) => {
if (socket.locals.rateLimited) {
next(new Error("Rate limited"));
} else {
next();
}
});Middleware error handling
const io = new ByteSocket({
middlewareTimeout: 5000, // ms before timeout error
onMiddlewareError: "close", // "ignore" | "close" | (error, socket) => void
onMiddlewareTimeout: "ignore",
});Lifecycle Events
// HTTP upgrade phase
io.lifecycle.onUpgrade((res, req, userData, context) => {
// Inspect headers, validate origin, etc.
// Throw or call res.end() to reject
});
// Socket open (fires after auth if configured)
io.lifecycle.onOpen((socket) => {
console.log(`${socket.id} connected`);
});
// Authentication success (fires after server confirms auth)
io.lifecycle.onAuthSuccess((socket) => {
console.log(`Socket ${socket.id} authenticated`);
});
// Authentication failure (fires when auth fails or times out)
io.lifecycle.onAuthError((socket, ctx) => {
console.error(`Auth failed for ${socket.id}:`, ctx.error);
});
// Raw incoming message
io.lifecycle.onMessage((socket, rawBuffer, isBinary) => {
console.log("Raw message received", rawBuffer);
});
// Socket closed
io.lifecycle.onClose((socket, code, message) => {
console.log(`${socket.id} closed with code ${code}`);
});
// Errors (decode, auth, middleware, etc.)
io.lifecycle.onError((socket, ctx) => {
// socket may be null if the error occurred before the socket was fully created (e.g., upgrade phase)
const socketId = socket?.id ?? "unknown";
console.error(`[${socketId}] Error in phase "${ctx.phase}":`, ctx.error);
});All lifecycle methods have on, once, and off variants:
io.lifecycle.onceOpen((socket) => console.log("First ever connection"));
io.lifecycle.offClose(myCloseHandler);
io.lifecycle.offClose(); // remove all close listenersSocket API
Every event handler and middleware receives a Socket instance:
// Unique identifier
socket.id; // UUID string
// Auth payload (set by your auth function)
socket.payload; // any (cast to your type)
// Arbitrary data store -- survives across middleware
socket.locals.requestId = randomUUID();
// HTTP metadata from upgrade request (convenience getters)
socket.query; // query string
socket.cookie; // Cookie header
socket.authorization; // Authorization header
socket.userAgent; // User-Agent header
socket.host; // Host header
socket.xForwardedFor; // X-Forwarded-For header
// The raw userData object (including any custom fields) is still available:
socket.userData; // full SocketData object
// Auth state
socket.isAuthenticated; // boolean
socket.isClosed; // boolean
// Send directly to this socket
socket.emit("welcome", { message: "Hello!" });
socket.sendRaw(buffer); // bypass serialization
// Room operations
socket.rooms.join("chat");
socket.rooms.leave("chat");
socket.rooms.list(); // string[]
socket.rooms.emit("chat", "message", { text: "Hi" });
// Broadcast to everyone (including this socket)
socket.broadcast("user:joined", { userId: socket.id });
// Close this connection
socket.close();
socket.close(1008, "Policy violation");Custom Socket Data
Extend SocketData to add your own typed fields, populated during the upgrade:
import { ByteSocket, SocketData } from "@bytesocket/uws";
interface AppSocketData extends SocketData {
tenantId: string;
}
const io = new ByteSocket<MyEvents, AppSocketData>({
auth: (socket, data, callback) => {
const tenant = lookupTenant(data.token);
if (!tenant) return callback(null, new Error("Unauthorized"));
// Attach to userData directly during upgrade via onUpgrade,
// or use socket.locals / socket.payload for runtime data
callback({ tenantId: tenant.id });
},
});Origin Validation
const io = new ByteSocket({
origins: ["https://example.com", "https://app.example.com"],
// Empty array (default) = allow all origins
});Serialization
// Binary (default) -- msgpackr, smallest payloads
const io = new ByteSocket({ serialization: "binary" });
// JSON -- plain text, easier to inspect/debug
const io = new ByteSocket({ serialization: "json" });
// Advanced msgpackr options
const io = new ByteSocket({
serialization: "binary",
msgpackrOptions: {
useFloat32: true,
bundleStrings: false,
},
});The serialization mode must match the client's
serializationoption.
Advanced: Manual Serialization
If you need to inspect, pre-encode, or bypass the automatic serialization, you can use the encode() and decode() methods.
⚠️ These are advanced APIs. Prefer
emit()andon()for type-safe, automatic encoding/decoding.
// Encode a structured payload (returns a string or Uint8Array)
const encoded = io.encode({ event: "chat", data: { text: "Hello" } });
// Broadcast the raw encoded payload to a room
io.rooms.publishRaw("lobby", encoded);
// Or send it to a specific socket
socket.sendRaw(encoded);
// Decode a raw incoming message (useful in lifecycle.onMessage)
io.lifecycle.onMessage((socket, rawBuffer, isBinary) => {
const decoded = io.decode(rawBuffer, isBinary);
console.log("Decoded message:", decoded);
});encode(payload)- uses the configuredserialization("json"or"binary").decode(message, isBinary?)- parses a raw WebSocket message back into an object. IfisBinaryis omitted, the format is auto-detected.
Caution:
encode()throws if the payload cannot be serialised (e.g., circular references or functions). Wrap it in a try-catch when dealing with untrusted data structures.
These methods give you full control when integrating with external systems or debugging the wire format.
Server Sockets Map
// Iterate all connected sockets
for (const [id, socket] of io.sockets) {
socket.emit("ping", undefined);
}
// Look up a specific socket
const socket = io.sockets.get(socketId);Destroy
io.destroy();Closes all connections and cleans up all resources. The instance cannot be reused.
After
destroy(), the WebSocket route remains registered on the uWS app, but it is backed by the destroyed instance and becomes inactive. To use the same path again, simply create a newByteSocketand callattach()— the new handler overwrites the old one automatically.
Full Configuration Reference
const io = new ByteSocket({
// Authentication
auth: (socket, data, callback) => {
callback({ userId: 1 });
},
authTimeout: 5000,
// Middleware
middlewareTimeout: 5000,
roomMiddlewareTimeout: 5000,
onMiddlewareError: "ignore", // "ignore" | "close" | (err, socket) => void
onMiddlewareTimeout: "ignore",
// Serialization
serialization: "binary", // "binary" | "json"
msgpackrOptions: {},
// CORS
origins: ["https://example.com"],
// Broadcast
broadcastRoom: "__bytesocket_broadcast__",
// Debug
debug: false,
// uWebSockets.js pass-through options
idleTimeout: 120000, // milliseconds, 0 = disabled
sendPingsAutomatically: true,
serverOptions: {
maxPayloadLength: 16 * 1024 * 1024,
compression: uWS.SHARED_COMPRESSOR,
maxLifetime: 120,
// any other uWS WebSocketBehavior option except
// upgrade, open, message, close, idleTimeout, sendPingsAutomatically
},
});Transport‑specific options (serverOptions)
All native uWebSockets.js settings (except idleTimeout and sendPingsAutomatically,
which are managed by ByteSocket) must be placed inside the serverOptions object:
const io = new ByteSocket({
serverOptions: {
maxPayloadLength: 64 * 1024,
compression: uWS.SHARED_COMPRESSOR,
closeOnBackpressureLimit: true,
},
});Transport‑specific uWebSockets.js options are provided via the serverOptions property. idleTimeout and sendPingsAutomatically are passed to uWebSockets.js as well. See {@link WebSocketServerOptions} for the full list of available settings.
License
MIT © 2026 Ahmed Ouda
- GitHub: @a7med3ouda
