
Security News
/Research
Popular node-ipc npm Package Infected with Credential Stealer
Socket detected malicious node-ipc versions with obfuscated stealer/backdoor behavior in a developing npm supply chain attack.
@bytesocket/node
Advanced tools
Framework-agnostic WebSocket server for Node.js using the native http.Server upgrade API
@bytesocket/nodeWebSocket server for ByteSocket built on the popular ws library.
âś… Works with any Node.js HTTP server -- Express, Fastify, Koa, NestJS, plain
http.createServer, and more.
next() / next(err) flowmsgpackr) out of the box# Server (Node.js backend)
npm install @bytesocket/node
# or
pnpm add @bytesocket/node
# or
yarn add @bytesocket/node
# Client (browser / Node.js frontend)
npm install @bytesocket/client
# or
pnpm add @bytesocket/client
# or
yarn add @bytesocket/client
No additional dependencies required — ws is included as a dependency of @bytesocket/node.
import http from "node:http";
import { ByteSocket } from "@bytesocket/node";
const server = http.createServer();
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(server, "/socket");
server.listen(3000, () => {
console.log("Listening on port 3000");
});
import express from "express";
import http from "node:http";
import { ByteSocket } from "@bytesocket/node";
const app = express();
const server = http.createServer(app);
const io = new ByteSocket();
io.attach(server, "/ws");
server.listen(3000, () => console.log("Server ready"));
import fastify from "fastify";
import { ByteSocket } from "@bytesocket/node";
const app = fastify({ logger: true });
const io = new ByteSocket();
io.attach(app.server, "/ws");
app.listen({ port: 3000 });
import Koa from "koa";
import http from "node:http";
import { ByteSocket } from "@bytesocket/node";
const app = new Koa();
const server = http.createServer(app.callback());
const io = new ByteSocket();
io.attach(server, "/ws");
server.listen(3000);
import { Module, OnModuleInit, OnModuleDestroy } from "@nestjs/common";
import { ByteSocket } from "@bytesocket/node";
import * as http from "node:http";
@Module({})
export class SocketModule implements OnModuleInit, OnModuleDestroy {
private io = new ByteSocket();
onModuleInit() {
// access the underlying HTTP server (Nest application adapter)
const app = this.app.getHttpAdapter().getInstance();
const server = app instanceof http.Server ? app : app?.server;
if (server) {
this.io.attach(server, "/ws");
console.log("WebSocket server attached");
}
// example listener
this.io.on("hello", (socket, data) => {
socket.emit("welcome", { message: `Hi ${data.name}` });
});
}
onModuleDestroy() {
this.io.destroy();
}
}
Note: The exact way to obtain the HTTP server depends on the platform adapter used (Express, Fastify, etc.).
For Express,app.getHttpServer()returns the underlyinghttp.Server. For Fastify, useapp.getHttpAdapter().getInstance().server.
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).
Use SocketEvents<T> directly with a single event map:
import { ByteSocket, SocketEvents } from "@bytesocket/node";
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();
});
Extend SocketEvents and override specific properties to differentiate emit/listen/room maps:
import { ByteSocket, SocketEvents } from "@bytesocket/node";
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.
Validate credentials when a client first connects. Until auth succeeds, no user messages are processed.
import { ByteSocket } from "@bytesocket/node";
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);
});
io.lifecycle.onOpen((socket) => {
socket.rooms.join("lobby");
socket.rooms.leave("lobby");
console.log(socket.rooms.list()); // ["__bytesocket_broadcast__"]
});
// 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" });
socket.rooms.bulk.join(["lobby", "notifications", "chat"]);
socket.rooms.bulk.leave(["lobby", "notifications"]);
socket.rooms.bulk.emit(["room1", "room2"], "alert", { msg: "Hello both!" });
// Single-room guard
io.rooms.lifecycle.onJoin((socket, room, next) => {
if (room === "admin" && !socket.payload?.isAdmin) {
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();
});
// 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 room
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();
}
});
const io = new ByteSocket({
middlewareTimeout: 5000, // ms before timeout error
onMiddlewareError: "close", // "ignore" | "close" | (error, socket) => void
onMiddlewareTimeout: "ignore",
});
By default, the server sends automatic pings and closes connections that remain idle for 120 seconds.
const io = new ByteSocket({
idleTimeout: 60000, // milliseconds of inactivity before termination (0 = disabled)
sendPingsAutomatically: true, // set to false to disable pings
});
The idle timer resets on every incoming message or pong. You can disable pings and timeouts entirely:
const io = new ByteSocket({
idleTimeout: 0,
sendPingsAutomatically: false,
});
// HTTP upgrade phase
io.lifecycle.onUpgrade((req, streamSocket, head, userData, wss) => {
// Inspect headers, throw or call streamSocket.destroy() to reject
});
// Socket open (fires after auth if configured)
io.lifecycle.onOpen((socket) => {
console.log(`${socket.id} connected`);
});
// Authentication success
io.lifecycle.onAuthSuccess((socket) => {
console.log(`Socket ${socket.id} authenticated`);
});
// Authentication failure
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, reason) => {
console.log(`${socket.id} closed with code ${code}`);
});
// Errors (decode, auth, middleware, etc.)
io.lifecycle.onError((socket, ctx) => {
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 listeners
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.url; // path, e.g. "/socket"
socket.query; // raw query string (without leading `?`)
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");
Extend SocketData to add your own typed fields, populated during the upgrade:
import { ByteSocket, SocketData } from "@bytesocket/node";
interface AppSocketData extends SocketData {
tenantId: string;
}
const io = new ByteSocket<MyEvents, AppSocketData>({
// ...
});
You can populate extra fields by overriding onUpgrade or using a custom auth function that stores values in socket.locals or socket.payload.
const io = new ByteSocket({
origins: ["https://example.com", "https://app.example.com"],
// Empty array (default) = allow all origins
});
// 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.
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 Buffer)
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
io.lifecycle.onMessage((socket, rawBuffer, isBinary) => {
const decoded = io.decode(rawBuffer, isBinary);
console.log("Decoded message:", decoded);
});
encode(payload) -- uses the configured serialization ("json" or "binary").decode(message, isBinary?) -- parses a raw WebSocket message. Handles fragmented messages automatically.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.
// 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);
const io = new ByteSocket();
io.attach(server, "/chat");
io.attach(server, "/notifications");
// Share the same underlying WebSocket server, but route messages based on path
The path is available via socket.url.
// Closes all connections, shuts down the WebSocket server.
// Note: The upgrade listener on the HTTP server is currently NOT removed.
// Instance cannot be reused.
io.destroy();
After destroy(), you can safely attach a new ByteSocket instance to the same HTTP server without conflicts.
const io = new ByteSocket({
// Authentication
auth: (socket, data, callback) => {
callback({ userId: 1 });
},
authTimeout: 5000,
// Middleware
middlewareTimeout: 5000,
roomMiddlewareTimeout: 5000,
onMiddlewareError: "ignore",
onMiddlewareTimeout: "ignore",
// Serialization
serialization: "binary", // "binary" | "json"
msgpackrOptions: {},
// CORS
origins: ["https://example.com"],
// Broadcast
broadcastRoom: "__bytesocket_broadcast__",
// Heartbeat
idleTimeout: 120000, // milliseconds, 0 = disabled
sendPingsAutomatically: true,
// Debug
debug: false,
// ws-specific options (see ServerOptions for full list)
serverOptions: {
maxPayload: 100 * 1024 * 1024,
perMessageDeflate: true,
skipUTF8Validation: false,
autoPong: true,
// … any other ws.ServerOptions except `noServer`/`port`/`server`/`host`/`backlog`/`path`
},
});
All ws server settings (e.g., maxPayload, perMessageDeflate, verifyClient, handleProtocols) are passed through the serverOptions field. The reserved keys noServer, port, server, host, backlog, and path are excluded because ByteSocket manages them internally.
const io = new ByteSocket({
serverOptions: {
maxPayload: 1024 * 1024,
perMessageDeflate: { threshold: 512 },
},
});
Any option in serverOptions is passed directly to the ws WebSocketServer constructor.
MIT © 2026 Ahmed Ouda
FAQs
Framework-agnostic WebSocket server for Node.js using the native http.Server upgrade API
The npm package @bytesocket/node receives a total of 10 weekly downloads. As such, @bytesocket/node popularity was classified as not popular.
We found that @bytesocket/node demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
/Research
Socket detected malicious node-ipc versions with obfuscated stealer/backdoor behavior in a developing npm supply chain attack.

Security News
TeamPCP and BreachForums are promoting a Shai-Hulud supply chain attack contest with a $1,000 prize for the biggest package compromise.

Security News
Packagist urges PHP projects to update Composer after a GitHub token format change exposed some GitHub Actions tokens in CI logs.