Protocol-agnostic WebSocket relay server built with Node.js, TypeScript, and msgpack. Designed for realtime multiplayer games, collaborative apps, and any application needing room-based WebSocket relay.
- Room-based architecture —
/ws/:roomIdwith auto-created rooms and configurable player caps - Protocol-agnostic relay — Server relays any msgpack message between peers without inspecting payloads
- Binary protocol (msgpack) — ~40% smaller payloads than JSON
- Instant relay — Messages forwarded immediately to peers (no server-side batching)
- Per-client rate limiting — Sliding window algorithm
- KeepAlive ping/pong — Automatic dead connection detection
- Origin allowlist — Configurable CORS protection
- Health + Metrics endpoints —
/healthand/metricsfor monitoring and autoscaling - Production Dockerfile — Multi-stage, non-root user, HEALTHCHECK
# Install dependencies
npm install
# Development (with hot reload)
npm run dev
# Production build + start
npm run build
npm startThis repo includes a tiny browser demo at examples/browser-demo.html that lets you connect two tabs and see relay messages in real time.
- Start the server:
npm run dev- Serve the demo page (any static server works):
cd examples
python3 -m http.server 3000- Open
http://localhost:3000/browser-demo.htmlin two tabs. - Click Connect in both tabs (defaults to
ws://localhost:8080/ws/lobby). - Type a message and click Send — the other tab will receive a
relay.
# Build and run
docker compose up --build
# Or manually
docker build -t node-ws-gameserver .
docker run -p 8080:8080 node-ws-gameserver| Variable | Default | Description |
|---|---|---|
PORT |
8080 |
Server listen port |
ALLOWED_ORIGINS |
* |
Comma-separated allowed origins |
KEEPALIVE_MS |
30000 |
Ping interval (ms) |
MAX_MESSAGES_PER_SECOND |
60 |
Per-client rate limit |
MAX_PLAYERS_PER_ROOM |
50 |
Room capacity |
node-ws-gameserver, bun-ws-gameserver, and cloudflare-ws-gameserver use the same msgpack binary relay protocol, so clients are backend-agnostic.
The server is protocol-agnostic — it manages rooms and connections, but treats game data as opaque payloads. Any client that speaks msgpack can use it: multiplayer games, collaborative tools, IoT dashboards, chat apps, etc.
- Client connects to
ws://host/ws/:roomId - Server auto-assigns a
playerIdand sendswelcomewith list of existing peers - Client sends any msgpack messages — server wraps each in a
relayenvelope and forwards to all other peers - When peers join/leave, server notifies all remaining peers
// Sent on connect
{ type: "welcome", playerId: string, peers: string[] }
// Peer lifecycle
{ type: "peer_joined", peerId: string }
{ type: "peer_left", peerId: string }
// Relayed game data from another peer (data is passed through untouched)
{ type: "relay", from: string, data: any }
// Keepalive response
{ type: "pong", nonce: string, serverTime: number }
// Errors (rate limit, room full, bad message)
{ type: "error", code: string, message: string }// Optional keepalive
{ type: "ping", nonce: string }
// ANYTHING ELSE is relayed to all other peers in the room.
// The server does not inspect or validate your game data.
// Examples:
{ type: "position", x: 1.5, y: 0, z: -3.2 }
{ type: "chat", text: "hello" }
{ type: "snapshot", pos: [0, 1, 0], rotY: 3.14, locomotion: "run" }import { encode, decode } from '@msgpack/msgpack';
const ws = new WebSocket('ws://localhost:8080/ws/lobby');
ws.binaryType = 'arraybuffer';
let myId: string;
ws.onmessage = (event) => {
const msg = decode(new Uint8Array(event.data));
switch (msg.type) {
case 'welcome':
myId = msg.playerId;
console.log(`Joined as ${myId}, peers:`, msg.peers);
break;
case 'peer_joined':
console.log(`${msg.peerId} joined`);
break;
case 'peer_left':
console.log(`${msg.peerId} left`);
break;
case 'relay':
// msg.from = peer ID, msg.data = whatever they sent
handlePeerData(msg.from, msg.data);
break;
}
};
// Send your game state (any shape you want)
setInterval(() => {
ws.send(encode({
type: 'position',
x: Math.random() * 10,
y: 0,
z: Math.random() * 10,
}));
}, 50);| Path | Method | Description |
|---|---|---|
/ws/:roomId |
WS | WebSocket connection (default room: "lobby") |
/health |
GET | Health check — status, rooms, connections, uptime |
/metrics |
GET | Detailed metrics — memory, messages/sec per room |
Click the deploy button at the top, or go to app.alternatefutures.ai — select this template and deploy to decentralized cloud in one click.
- Click the "Deploy on Railway" button above (GitHub import)
- Connect your GitHub account if prompted
- Deploy — Railway reads
railway.tomlautomatically
docker build --platform linux/amd64 -t node-ws-gameserver .
docker run -p 8080:8080 -e PORT=8080 node-ws-gameserver| Repo | Runtime | Deploy Target |
|---|---|---|
| node-ws-gameserver | Node.js 20 + ws |
Docker, Railway, DePIN, any host |
bun-ws-gameserver |
Bun native WS | Docker, Railway, DePIN, any host |
cloudflare-ws-gameserver |
Cloudflare Workers + DO | Cloudflare edge (global) |
All three implement the same msgpack relay protocol. Clients connect to any of them by changing the server URL.
MIT