OPEN SOURCE · MIT · NODE.JS

Stop writing WebSocket plumbing.

Build real-time multiplayer for Phaser, PixiJS, Three.js, or any browser game. Open-source Node.js framework — rooms, actors, typed binary protocol. Free for commercial use, forever.

  • Rooms + Actorscore2 concepts, 1 mental model
  • exp-backoffreconnectbrowser client, automatic
  • allow-listoriginCSWSH protection on by default
SEE IT IN ACTION

Real-time rooms in minutes.

Define your room on the server. The client subscribes to topics. Frames flow in both directions over a single binary protocol.

TypeScript
// GameRoom.ts
import { Room, type Actor } from '@rivalis/core'

type PlayerData = { name: string; score: number }

export class GameRoom extends Room<PlayerData> {

    protected override presence = true

    protected override onCreate() {
        this.bind('move', this.onMove)
    }

    private onMove(actor: Actor<PlayerData>, payload: Uint8Array) {
        this.broadcast('move', payload)
    }
}
Core model

Rooms & Actors

Model your game as rooms full of actors with a clean lifecycle. Two concepts, one mental model.

Messaging

Topic-based messaging

Bind a topic to a handler, broadcast or unicast — no manual switch statements.

Security

Pluggable auth

Validate any ticket — JWT, session token, anything — and attach typed actor data.

Wire format

Binary wire protocol

A typed { topic, payload } format keeps clients and servers in lockstep.

Transport

WebRTC P2P transport

The same room code runs over a direct peer-to-peer connection — players connect to each other while your server just helps them find one another. Lower latency, near-zero hosting cost for casual and co-op.

Scale

Fleet orchestration

Tie many Rivalis servers into one cluster and place rooms on the least-loaded one — a live in-memory view of the fleet, with no database to run.

Developer experience

TypeScript-first

Strict types end-to-end. Actor data, room handlers, and the wire format share generics.

License

Free & open source

MIT licensed. Free forever, even for commercial games. No per-seat, no per-CCU. Your server, your rules.

Hardening

Origin allow-lists

CSWSH protection on by default — tunable per transport. 64 KiB frame cap, 30/s token-bucket rate limit, 30 s heartbeat with two-miss disconnect.

USE CASES

What you can build.

Anywhere multiple humans need to share state in real time.

01
Game servers
Phaser, PixiJS, Three.js, Babylon.js — arena, .io, party, MMO zones, turn-based with presence and tick-rate broadcasts.
02
Live apps
Whiteboards, editors, dashboards, chat — every interaction is a topic frame.
03
Real-time ops
Auctions, IoT panels, dispatch, live polls — server-authoritative state with rate limiting.
SCALE BEYOND ONE SERVER

Many servers, one fleet.

One Rivalis server is plenty to start. When you outgrow it — more players, more regions, more rooms than a single process should hold — @rivalis/fleet ties many servers together so clients always find the right one.

When do you actually need one?

Honestly — probably not on day one. A single Rivalis server happily holds thousands of players, so start there and don't think about fleets until the numbers push back. You've outgrown one box when a single process can't keep up: more concurrent players, rooms, or regions than one machine should carry — or when you want players in Europe and the US to land on servers close to them. A fleet is the step after one server, not a prerequisite for your first.

01
Every server checks in
A tiny FleetAgent embeds in each Rivalis server and reports its rooms, players, and free capacity.
02
The orchestrator keeps the map
One live view of every server and room across the cluster — rebuilt from check-ins, no database to run.
03
Rooms land where they fit
Ask for a "match room" and the orchestrator picks the least-loaded server, then hands your client the URL to connect to.

A fleet

Your group of game servers and all the rooms running on them — one logical cluster instead of a pile of separate processes.

The Orchestrator

The brain. It knows which servers are alive, what rooms run where, and creates or destroys rooms on any of them — embed it in your own code or run the bundled binary.

The FleetAgent

The little reporter inside each server. It tells the orchestrator what it is running and quietly carries out create/destroy commands sent back to it.

The three pieces in code.

Embed an agent in every server, run one orchestrator, then ask it to place rooms.

TypeScript
// fleet-agent.ts — embeds in each game server
import { Rivalis } from '@rivalis/core'
import { FleetAgent } from '@rivalis/fleet'
import { MatchRoom } from './MatchRoom'

const rivalis = new Rivalis({ /* transports, authMiddleware */ })
rivalis.rooms.define('match', MatchRoom)

const agent = new FleetAgent(rivalis, {
    url: 'ws://orchestrator.internal:7350',     // where the orchestrator listens
    key: process.env.FLEET_AGENT_KEY!,          // agent key (sent via WS subprotocol)
    endpointUrl: 'wss://eu1.game.example.com',  // URL handed to game clients
    name: 'eu1',
    labels: { region: 'eu' },
    capacity: { maxConnections: 2000, maxRooms: 100 }
})

// reports rooms + capacity, then runs create/destroy commands pushed back to it
await agent.connect()

// SIGTERM → drain → wait until empty → disconnect → rivalis.shutdown()
agent.enableGracefulShutdown({ emptyTimeoutMs: 60_000 })

It only places rooms inside servers you are already running — spinning machines up and down stays with your platform (k8s, Agones, autoscalers), and matchmaking is something you build on top. In-memory, restart-safe, MIT.

PEER-TO-PEER

Let players talk directly.

With WebRTC peer-to-peer, players connect straight to each other instead of routing every frame through your server. Your server only helps peers find each other once — after that the game traffic skips it entirely. That means near-zero hosting cost and lower latency, which is exactly what casual and co-op games want.

Same room, one-line switch.

The room code does not change. Swap WSClient for RTCClient and point it at a @rivalis/signal server — the ticket and topics stay identical.

TypeScript
// game.ts — the room code is identical; only the client class changes.
import { WSClient, RTCClient } from '@rivalis/browser'

// Before — classic client/server: every frame is routed through your server.
const client = new WSClient('wss://eu1.game.example.com', { reconnect: true })

// After — peer-to-peer: your server only helps peers find each other,
// then frames travel directly between players.
const client = new RTCClient('wss://signal.game.example.com', { reconnect: true })

// Everything below stays exactly the same — same ticket, same topics.
await client.connect(ticket)                     // ticket: "roomId|playerName"
client.on('chat', (payload) => render(decode(payload)))
client.send('chat', encode({ text: 'gg!' }))

Three ways to wire peers together.

Pick the topology that fits your game — from a simple mesh to a single peer running the whole simulation.

Full mesh

Every peer connects to every other peer directly, and Rivalis is used only for discovery. No node is in charge.

Use it when you are building a small chat or co-op game with up to 10 peers.

Host star

One peer becomes the host and everyone else connects only to it. The host relays messages out to the rest — no per-pair links.

Use it when one peer should relay for everyone instead of every pair wiring up.

Host authoritative

One peer runs the actual game: it owns the shared state, applies inputs on a fixed tick, and broadcasts snapshots — including a catch-up sync for late joiners.

Use it when one peer should run the simulation with a tick loop and late-join sync.

A browser host is great for casual and co-op play. For competitive games you still want an authoritative server — keep those rooms on @rivalis/core. MIT, same wire protocol either way.

SIGNALING & TURN

Peer-to-peer still needs a tiny introducer.

“Peer-to-peer” sounds like there is no server at all — but two browsers can’t find each other on their own. They need a quick introduction first. That introduction is signaling, and it’s the one small piece of Rivalis that still runs as a server. The good news: it’s tiny, and it steps out of the way the moment players are connected.

Signaling: the switchboard

Two browsers can only connect directly after they swap connection details (SDP/ICE). @rivalis/signal relays that handshake and nothing else — like a switchboard operator who connects the call, then hangs up. Gameplay traffic never passes through it.

It also picks the host

Peers join the signaling room in order, and the first one in becomes the host. If that host leaves, the next-oldest peer is elected automatically — so there is always exactly one peer in charge, with no extra coordination on your side.

TURN: the fallback relay

Some networks block direct connections. When that happens a TURN relay (coturn) forwards the traffic instead. The signal server is not the relay — it only mints short-lived TURN credentials so peers can use coturn when they need it.

A signal server is a few lines.

It’s just a small Rivalis app. Give it a port and a ticket secret and it’s relaying handshakes — point your RTCClient at it and players take it from there.

TypeScript
// signal.ts — stand up a signaling server in a few lines.
import { SignalServer } from '@rivalis/signal'

// Constructing with a port starts listening immediately — no extra step.
const server = new SignalServer({
    port: 9000,
    secrets: [process.env.SIGNAL_SECRET!], // ticket secret(s); rotate freely
})

// Shut down cleanly on exit.
process.on('SIGTERM', () => server.shutdown({ timeoutMs: 10_000 }))

// In production you also run a coturn sidecar; the signal server just mints
// short-lived TURN credentials for it (ICE_TURN_URLS / ICE_TURN_SECRET).

That’s the whole server side of P2P: relay the handshake, pick a host, hand out TURN credentials. For production you add a coturn sidecar next to it as the actual relay. MIT, same wire protocol as the rest of Rivalis.

DEMOS

Three apps. Read the source.

Each demo is a few hundred lines — clone, run, hack.

Chat lobby

Topic-based broadcasts, presence join/leave, typed actor data.

Shared counter

Smallest possible room — one value, all actors see every change.

Pac-Man (2P)

Two-player real-time, server-authoritative ticks, binary payloads.

HOW RIVALIS COMPARES

Lightweight by design.

Both work. Pick by philosophy — not features.

vs Colyseus

  • Colyseus auto-syncs state via Schema classes — great when you want the framework to own state.
  • Rivalis gives you raw binary frames — great when you want full control over the wire.
  • Two concepts (Rooms + Actors), zero schema decorators, drops into any http.Server.

vs From scratch

  • Heartbeats, token-bucket rate limits, backpressure, exponential-backoff reconnect — built in
  • Origin allow-lists for CSWSH protection on by default
  • TypeScript-native, zero hidden runtime dependencies — ship in days, not months

Which setup do I pick?

  • P2P (WebRTC): cheapest, lowest latency for casual/co-op — your server only does discovery, but a peer host can’t be trusted for competitive play.
  • One dedicated server: server-authoritative and simple — the right default until you outgrow a single process.
  • Fleet: many servers in one cluster, each room placed on the least-loaded box — for more players, regions, or rooms than one server should hold.

Both are MIT. Both are Node.js. Try both — neither is wrong.

FOR AI CODING ASSISTANTS

Teach your AI to write Rivalis.

Rivalis ships a single-file skill manifest at https://rivalis.dev/SKILL.md. Load it into Claude Code, Cursor, or any agent with a custom-instructions slot, and the assistant will know the recipes, pitfalls, close codes, and security defaults — so the code it generates actually compiles and follows the framework rules. Same approach Phaser uses with its own AI skill files — load once, the assistant follows the rules.

What it is

A YAML-fronted Markdown file describing when to use Rivalis, the minimal server & client, the wire protocol, all four room recipes, the auth middleware pattern, close codes, and rate-limiting defaults — everything a coding agent needs in one fetch.

Why it helps

Without a skill, AI agents guess. They forget that __-prefixed topics are reserved, or that rooms must be defined before connections. The skill encodes those rules — your agent stops inventing APIs and starts shipping working code.

Always current

The file is served from the same domain as this page. Re-fetch it anytime to pull the latest recipes — or pin a copy into your repo if you want a frozen reference.

CLAUDE CODEAuto-loaded from ~/.claude/skills/
Bash
# Install the Rivalis skill into Claude Code
mkdir -p ~/.claude/skills/rivalis
curl -fsSL https://rivalis.dev/SKILL.md -o ~/.claude/skills/rivalis/SKILL.md

Once installed, Claude Code picks up the skill on its own — no config, no restart. Project-scoped install: drop it under .claude/skills/rivalis/SKILL.md at the repo root instead.

CURSOR / WINDSURFOr any project-rules slot
Bash
# Drop the skill next to your repo so the agent picks it up
mkdir -p .cursor/rules
curl -fsSL https://rivalis.dev/SKILL.md -o .cursor/rules/rivalis.md

The format is plain Markdown — paste it into any agent that accepts custom rules, system prompts, or knowledge files.

ANY ASSISTANTPoint it at the URL in a prompt
Bash
Read the Rivalis skill at https://rivalis.dev/SKILL.md
and use it as the source of truth when you write or review code that
imports @rivalis/core or @rivalis/browser.

For chat UIs without a skills system (ChatGPT, Gemini, etc.), the simplest path is to ask the model to fetch the URL and treat it as the framework's reference. The file is small enough to paste inline if web-fetch is unavailable.

JOIN THE COMMUNITY

Built in the open.

Rivalis is early but stable. The author is actively looking for feedback on API ergonomics — anything that feels off, missing, or weird. Open an issue or DM.

GitHub

Source code · star · fork

Send feedback

API ergonomics — what feels off, missing, or weird

Issues

Bug reports & feature requests

Author

Built by @kalevski

OPEN SOURCEMIT · FREE FOR COMMERCIALNODE.JS

Build something that talks back.

Read the docs. Clone a demo. Ship a prototype this weekend.