WebRTC signaling, proprietary SFU, adaptive bitrate, multi-region orchestration, E2EE, compliance, observability, and enterprise administration for Node.js.
Built and maintained by Himanshu Pandey.
webrtc-rooms is a complete real-time video/audio platform in a single npm package. It gives you a production signaling server, a native zero-dependency SFU with simulcast and adaptive bitrate, multi-region orchestration with automatic failover, HMAC-signed session persistence, Redis pub/sub scaling, end-to-end encryption, SOC2/HIPAA audit logging, GDPR consent and data residency enforcement, distributed tracing, Prometheus metrics, SLO health monitoring, threat detection, backpressure-aware load shedding, structured moderation, signed webhooks, and a CLI that scaffolds any of it in under a minute.
npm install webrtc-roomsRequires Node.js 18 or later. No mandatory external dependencies beyond ws and uuid.
| Layer | Modules |
|---|---|
| Signaling core | SignalingServer, Room, Peer, createServer |
| Session management | SessionManager — HMAC-signed reconnect, cross-process resume |
| Proprietary SFU | NativeSFUEngine, SFUOrchestrator, AdaptiveBitrateController, RegionRouter |
| Auth | PolicyEngine — signed capability tokens · RoomToken |
| Security | ThreatDetector — 8 threat models · AuditLogger — SOC2/HIPAA |
| Observability | MetricsCollector + Prometheus · Tracer · AlertManager · HealthMonitor |
| Reliability | EventReplay · BackpressureController · HealthMonitor |
| Scaling | RedisAdapter · RoomPersistence |
| Compliance | ConsentFlow · DataResidency · RetentionPolicy |
| Moderation | ModerationBus |
| Recording | RecordingPipeline |
| E2EE | E2EKeyExchange — ECDH key distribution |
| Admin | AdminAPI · GovernanceEndpoints — 15 compliance endpoints |
| CLI | webrtc-rooms init · simulate · benchmark · health |
const { createServer } = require("webrtc-rooms");
const server = createServer({ port: 3000 });
server.on("peer:joined", (peer, room) => {
console.log(`${peer.metadata.displayName} joined "${room.id}"`);
});For a production setup with all v2 features, scaffold a project:
npx webrtc-rooms init my-app --template enterprise
cd my-app && npm install && node server.jsBrowser A webrtc-rooms v2 server Browser B
│ │ │
├── { join, roomId } ──────►│ │
│◄─ { room:joined, peers } │ │
│ │◄────── { join, roomId } ──────┤
│◄─ { peer:joined } │──── { room:joined, peers } ──►│
│ │ │
├── { offer, target:B } ───►│──── { offer, from:A } ───────►│
│◄─ { answer, from:B } ─────│◄─── { answer, target:A } ─────┤
│ │ │
◄──────────── ICE trickle ──┼──────── ICE trickle ─────────►│
│ │
◄═══════════ direct P2P media (P2P) or via NativeSFUEngine ►│
In P2P mode the server only routes signaling — media never touches it. Attach
NativeSFUEngine for rooms with many participants; the SFU receives and selectively
forwards tracks, while AdaptiveBitrateController picks the right simulcast layer
for each subscriber based on live network conditions.
- Installation
- CLI
- API reference
- createServer
- SignalingServer
- Room
- Peer
- SessionManager
- PolicyEngine
- NativeSFUEngine
- SFUOrchestrator
- AdaptiveBitrateController
- RegionRouter
- ThreatDetector
- AuditLogger
- MetricsCollector
- Tracer
- AlertManager
- HealthMonitor
- EventReplay
- BackpressureController
- ModerationBus
- RecordingPipeline
- ConsentFlow
- DataResidency
- RetentionPolicy
- E2EKeyExchange
- RedisAdapter
- RoomPersistence
- RateLimiter
- AdminAPI
- GovernanceEndpoints
- Wire protocol
- Authentication
- Reconnection
- Multi-server scaling
- Room persistence
- SFU mode
- Adaptive bitrate
- End-to-end encryption
- Observability
- Security
- Compliance
- Moderation
- TypeScript
- Mounting on Express
- Running tests
- Contributing
- License
npm install webrtc-roomsOptional peer dependencies — install only what you use:
npm install redis # or: npm install ioredis — for RedisAdapter / RoomPersistence
npm install mediasoup # legacy SFU adapter (NativeSFUEngine needs no extra install)For recording, ffmpeg must be on PATH.
# Scaffold a new project
npx webrtc-rooms init my-app
npx webrtc-rooms init my-app --template enterprise # full stack
# Start a local multi-process cluster for development
npx webrtc-rooms simulate --processes 3 --redis redis://localhost:6379
# Run a reproducible load test
npx webrtc-rooms benchmark --peers 200 --rooms 20 --duration 60
# Check a running server
npx webrtc-rooms health http://localhost:4000/admin/health --secret my-tokenTemplates
| Template | What's included |
|---|---|
basic |
Minimal signaling server — 30 lines |
advanced |
Auth + rate limiting + admin API + metrics |
sfu |
NativeSFUEngine + SFUOrchestrator + AdaptiveBitrateController |
enterprise |
Full stack — SFU, Redis, persistence, E2EE, compliance, observability |
const { createServer } = require("webrtc-rooms");
const server = createServer({
port: 3000,
server: httpServer, // attach to an existing http.Server
maxPeersPerRoom: 50,
autoCreateRooms: true,
autoDestroyRooms: true,
reconnectTtl: 15_000, // ms (0 = off)
beforeJoin: async (peer, roomId) => {
/* see Authentication */
},
});// Events
server.on("listening", ({ port }) => {});
server.on("peer:connected", (peer) => {});
server.on("peer:joined", (peer, room) => {});
server.on("peer:left", (peer, room) => {});
server.on("peer:reconnected", (peer, room) => {});
server.on("room:created", (room) => {});
server.on("room:destroyed", (room) => {});
server.on("join:rejected", (peer, reason) => {});
// Methods
server.createRoom("standup", { metadata: { topic: "Daily standup" } });
const room = server.getRoom("standup");
server.kick(peerId, "Policy violation");
server.stats(); // → { rooms, peers, roomList }
await server.close();room.broadcast({ type: "server:notice", text: "Recording started." });
room.broadcast({ type: "data", payload: "hello" }, { exclude: [peer.id] });
room.setMetadata({ topic: "New topic", recordingActive: true });
room.getState(); // → { id, metadata, peers[], createdAt }
room.size; // number of peers
room.isEmpty; // booleanpeer.id; // UUID v4
peer.state; // 'connecting' | 'joined' | 'reconnecting' | 'closed'
peer.metadata; // { displayName, role, userId, ... }
peer.isActive; // true when state === 'joined'
peer.connectedAt; // Unix timestamp ms
peer.setMetadata({ displayName: "Alice", token: null }); // null removes the key
peer.send({ type: "custom:event", data: "anything" });
peer.close(1008, "Policy violation");
peer.toJSON(); // → { id, roomId, state, metadata }
Peer.State.CONNECTING; // 'connecting'
Peer.State.JOINED; // 'joined'
Peer.State.RECONNECTING; // 'reconnecting'
Peer.State.CLOSED; // 'closed'HMAC-SHA256 signed reconnect tokens with cross-process Redis resume and session migration between regions.
const { SessionManager } = require("webrtc-rooms");
const sessions = new SessionManager({
reconnectTtl: 30_000, // ms session stays alive after socket drop
maxQueueSize: 64, // messages buffered during suspension
secret: process.env.SESSION_SECRET, // shared across processes
redis, // optional — enables cross-process resume
region: "ap-south-1",
});
sessions.attach(server);
// Manual resume (called automatically on peer:reconnected)
const resumed = await sessions.resume(token, roomId);
// Events
sessions.on("session:created", (session) => {});
sessions.on("session:suspended", (session, room) => {});
sessions.on("session:resumed", (session) => {});
sessions.on("session:expired", (session) => {});
sessions.on("session:migrated", (session, targetRegion) => {});Signed room policy tokens with capability enforcement. Integrates into
beforeJoin automatically — no custom hook needed.
const { PolicyEngine } = require("webrtc-rooms");
const policy = new PolicyEngine({
secret: process.env.POLICY_SECRET,
required: true, // reject peers without a valid token
defaultCaps: ["subscribe", "data"],
});
policy.attach(server);
// Issue a token (typically in your API layer, not the signaling server)
const token = policy.issue({
sub: user.id,
roomId: "engineering",
role: "moderator",
caps: ["publish", "subscribe", "kick", "data"],
expiresIn: 7_200_000,
});
// Browser sends token in join metadata:
// ws.send(JSON.stringify({ type: 'join', roomId, metadata: { policyToken: token } }))
// Check capabilities on a peer
policy.hasCap(peer, "kick"); // → boolean
policy.on("policy:violation", ({ peer, roomId, code }) => {
console.warn("Policy violation", code, "from", peer.id);
});Capabilities: publish · subscribe · kick · record · moderate · data · admin
(admin implies all others)
Proprietary SFU engine — zero external SFU dependencies. Handles publish/subscribe, simulcast layer selection, and full producer/consumer lifecycle inside the Node.js process.
const { NativeSFUEngine } = require("webrtc-rooms");
const sfu = new NativeSFUEngine({
region: process.env.REGION || "default",
listenIp: "0.0.0.0",
announcedIp: process.env.PUBLIC_IP,
rtcMinPort: 10000,
rtcMaxPort: 59999,
enableSimulcast: true,
});
await sfu.init();
sfu.attach(server);
sfu.stats();
// → { region, rooms, totalProducers, totalConsumers, totalTransports, initialized }Browser signals (sent through the data relay using __sfu discriminator):
// Publish a track
ws.send(
JSON.stringify({
type: "data",
payload: { __sfu: "publish", kind: "video", trackId: "v1" },
}),
);
// Subscribe to a remote producer
ws.send(
JSON.stringify({ type: "data", payload: { __sfu: "subscribe", producerId } }),
);
// Select simulcast layer
ws.send(
JSON.stringify({
type: "data",
payload: { __sfu: "layers", consumerId, layer: "high" },
}),
);
// Pause / resume
ws.send(
JSON.stringify({ type: "data", payload: { __sfu: "pause", producerId } }),
);
ws.send(
JSON.stringify({ type: "data", payload: { __sfu: "resume", producerId } }),
);Server → client SFU messages: sfu:ready · sfu:published · sfu:subscribed ·
sfu:peer:published · sfu:peer:unpublished · sfu:consumer:closed ·
sfu:layer:changed · sfu:paused · sfu:resumed
Manages a fleet of NativeSFUEngine instances across regions with health
monitoring, automatic failover, and room migration.
const { SFUOrchestrator, NativeSFUEngine } = require("webrtc-rooms");
const orchestrator = new SFUOrchestrator({
server,
defaultRegion: "ap-south-1",
healthCheckIntervalMs: 10_000,
maxRoomsPerSFU: 500,
fallbackToP2P: true, // P2P if all SFUs down
});
orchestrator.register(
"ap-south-1",
new NativeSFUEngine({ region: "ap-south-1" }),
);
orchestrator.register(
"eu-west-1",
new NativeSFUEngine({ region: "eu-west-1" }),
);
await orchestrator.init();
// Manual room migration
await orchestrator.migrateRoom("room-123", "eu-west-1");
orchestrator.stats();
// → [{ region, health, roomCount, failCount, initialized, adapterStats }]
orchestrator.on("sfu:down", (region) => {
/* alert */
});
orchestrator.on("failover", (region, rooms) => {});
orchestrator.on("room:migrated", (roomId, from, to) => {});Health states: healthy · degraded · down
Per-subscriber simulcast layer selection driven by live RTCP feedback signals. Protects active speakers and screen-share tracks. Mobile-first tuning.
const { AdaptiveBitrateController } = require("webrtc-rooms");
const abc = new AdaptiveBitrateController({
sfuEngine: sfu,
upgradeHoldMs: 5_000, // hysteresis — hold before upgrading
mobileFirst: true, // mobile peers start at lower tier
protectActiveSpeaker: true, // never downgrade active speaker below mid
protectScreenShare: true,
});
abc.attach(server);
// Mark a producer as active speaker (e.g. from VAD detection)
abc.setActiveSpeaker(producerId);
abc.clearActiveSpeaker(producerId);
abc.markScreenShare(producerId);
abc.stats();
// → [{ consumerId, peerId, score, currentLayer, targetLayer, deviceType, pliCount, nackCount }]
abc.on("layer:changed", ({ consumerId, peerId, from, to, score }) => {});
abc.on("audio:only:hint", ({ consumerId, peerId, score }) => {});Browser sends feedback via data relay:
ws.send(
JSON.stringify({
type: "data",
payload: { __sfu: "feedback", type: "pli", consumerId },
}),
);
ws.send(
JSON.stringify({
type: "data",
payload: { __sfu: "feedback", type: "remb", consumerId, bitrate: 800000 },
}),
);
ws.send(
JSON.stringify({
type: "data",
payload: { __sfu: "feedback", type: "nack", consumerId, count: 2 },
}),
);Assigns peers and rooms to regions using latency, affinity, residency, load, or manual strategies. Emits migration hints when a peer would be better served by a different region.
const { RegionRouter } = require("webrtc-rooms");
const router = new RegionRouter({
server,
localRegion: "us-east-1",
regions: ["us-east-1", "eu-west-1", "ap-south-1"],
mode: "affinity", // latency | affinity | residency | load | manual
});
router.attach();
router.assignRoomRegion("my-room", "eu-west-1");
router.getPeerRegion(peerId); // → 'eu-west-1'
router.getRoomRegion(roomId); // → 'eu-west-1'
router.on("peer:should:migrate", ({ peerId, currentRegion, targetRegion }) => {
// Send a reconnect hint to the peer's browser
});Real-time in-process abuse detection across 8 threat models. No external service required.
const { ThreatDetector } = require("webrtc-rooms");
const detector = new ThreatDetector({
server,
whitelist: ["127.0.0.1", "::1"],
thresholds: {
maxConnPerMinPerIp: 30,
maxSignalsPerSecPerPeer: 50,
maxPayloadBytes: 65_536,
},
});
detector.attach();
detector.on("threat", ({ level, threat, peer, ip, ts }) => {
myLogger.warn("[threat]", level, threat, ip);
});
// Manual ban management
detector.ban("1.2.3.4", 300_000);
detector.unban("1.2.3.4");
detector.bans(); // → [{ ip, expiresIn }]Threat levels: warn · throttle · kick · ban
Threat models: connection flood · signal flood · room cycling · SDP spam · data abuse · metadata poisoning · idle timeout · amplification
Append-only structured audit log. Writes NDJSON with automatic file rotation. SOC2/HIPAA-ready with optional IP redaction.
const { AuditLogger } = require("webrtc-rooms");
const audit = new AuditLogger({
server,
filePath: "./logs/audit.ndjson",
maxFileSizeBytes: 100 * 1024 * 1024, // 100 MB then rotate
ringSize: 10_000, // in-memory entries
redactIp: false, // true for GDPR-compliant logging
sink: async (entry) => {
// custom async sink
await myLogPlatform.ingest(entry);
},
});
audit.attach();
// Query the in-memory ring buffer
audit.query({ event: "peer:joined", roomId: "standup", limit: 50 });
audit.query({ peerId: "abc-123", since: Date.now() - 3600_000 });
// Manual entries (e.g. from PolicyEngine, AdminAPI)
audit.log("room:policy:changed", { roomId, actorId, change });
audit.on("entry", (entry) => {});Auto-logged events: peer:connected · peer:joined · peer:left ·
peer:reconnected · join:rejected · room:created · room:destroyed
Per-room QoS metrics with P50/P95/P99 latency histograms and Prometheus export.
const { MetricsCollector } = require("webrtc-rooms");
const metrics = new MetricsCollector({ server });
metrics.attach();
// Full snapshot
const snap = metrics.snapshot();
// → { timestamp, uptimeMs, system, server: { rooms, peers, joinsTotal, ... }, rooms: [...] }
// Per-room QoS
const rm = metrics.roomSnapshot("standup");
// → { peersCurrent, peersPeak, joinsTotal, reconnectSuccessRate, joinLatency: { p50, p95, p99 }, ... }
// Prometheus text format (serve on /metrics)
app.get("/metrics", (req, res) => {
res.type("text/plain").send(metrics.toPrometheus());
});Lightweight distributed tracing with OpenTelemetry-compatible span model. Zero SDK dependency.
const { Tracer } = require("webrtc-rooms");
const tracer = new Tracer({
server,
mode: "buffer", // 'console' | 'buffer' | 'noop'
exporter: async (span) => {
await jaeger.report(span);
},
});
tracer.attach();
// Manual span
const span = tracer.startSpan("db.query", traceId, { table: "users" });
span.addEvent("cache:miss");
span.end("ok");
// Query buffered spans
tracer.getSpans({ limit: 100, name: "peer.join" });
tracer.getTraceId(peerId); // → traceId stringWires health, threat, SFU, and backpressure events into configurable channels.
const { AlertManager } = require("webrtc-rooms");
const alerts = new AlertManager({
channels: [
{ type: "console" },
{ type: "webhook", url: "https://hooks.slack.com/..." },
{ type: "custom", handler: async (alert) => myPagerDuty.trigger(alert) },
],
suppressionWindowMs: 60_000, // deduplicate repeat alerts
});
alerts.attachHealthMonitor(health);
alerts.attachThreatDetector(threats);
alerts.attachSFUOrchestrator(orchestrator);
alerts.attachBackpressure(bp);
// Manual alert
alerts.alert({
event: "custom:event",
severity: "warning",
message: "Disk filling up",
});
alerts.recent(20); // last N alertsSLO tracking against configurable targets. Fires breach and recovery events.
const { HealthMonitor } = require("webrtc-rooms");
const health = new HealthMonitor({ server, metrics });
health.attach();
health.report(); // → { healthy, breaches: [{ slo, current, target }] }
health.isHealthy(); // → boolean
health.breaches(); // → array of active breaches
health.on("slo:breach", ({ slo, current, target }) => {});
health.on("slo:recovered", ({ slo }) => {});Default SLOs: join success ≥ 99.5% · P95 join latency ≤ 500ms · reconnect success ≥ 95% · heap ≤ 85%
Per-room ordered event log. Replays missed events to reconnecting peers with sequence-number deduplication.
const { EventReplay } = require("webrtc-rooms");
const replay = new EventReplay({
server,
capacity: 10_000, // max events in memory
replayTtlMs: 300_000, // events older than this not replayed
replayOnReconnect: true, // automatic replay on peer:reconnected
});
replay.attach();
// Manual replay
const count = replay.replayToPeer(peerId, lastSeenSeq);
// Query the log
replay.roomEvents("standup", afterSeq);
replay.stats(); // → { size, seq, oldestSeq }
replay.on("replayed", ({ peerId, roomId, count, fromSeq, toSeq }) => {});The client stores __seq from each broadcast message and sends lastSeenSeq
in the reconnect message. The server replays everything the client missed.
Graduated load protection across 5 levels. Gates joins and sheds peers under critical memory pressure.
const { BackpressureController } = require("webrtc-rooms");
const bp = new BackpressureController({
server,
maxPeers: 10_000,
sampleIntervalMs: 2_000,
enableLoadShedding: true,
});
bp.attach();
bp.status();
// → { level, heapRatio, peerRatio, peerCount, maxPeers, heapUsedMb, heapTotalMb }
bp.on("load:elevated", (info) => {});
bp.on("load:high", (info) => {});
bp.on("load:critical", (info) => {});
bp.on("load:shedding", (info) => {});Load levels: normal → elevated → high → critical → shedding
At critical: new joins are rejected with SERVER_OVERLOADED.
At shedding: lowest-priority peers (viewers, no role) are disconnected.
Structured moderation actions with capability enforcement and searchable log.
const { ModerationBus } = require("webrtc-rooms");
const modbus = new ModerationBus({ server, policyEngine: policy });
modbus.attach();
// Actions
modbus.mute({ roomId, targetId, actorId, reason });
modbus.unmute({ roomId, targetId, actorId });
modbus.kick({ roomId, targetId, actorId, reason });
modbus.warn({ roomId, targetId, actorId, reason });
modbus.lockRoom(roomId, actorId);
modbus.unlockRoom(roomId, actorId);
// State
modbus.isMuted(roomId, peerId); // → boolean
modbus.isLocked(roomId); // → boolean
modbus.log({ roomId, limit: 50 }); // → ModerationEvent[]
modbus.on("muted", (info) => {});
modbus.on("kicked", (info) => {});
modbus.on("room:locked", (info) => {});
modbus.on("abuse:reported", (info) => {});Peers without the moderate capability cannot mute or kick others when
policyEngine is attached.
Structured recording with session tracking, completion index, search, and optional cloud upload hook.
const { RecordingPipeline } = require("webrtc-rooms");
const pipeline = new RecordingPipeline({
outputDir: "./recordings",
format: "webm", // 'webm' | 'mp4'
autoRecord: true, // auto-start on peer:joined
onUpload: async (session) => {
await s3.upload(session.filePath, session.roomId);
},
});
pipeline.attach(server);
pipeline.active(); // → currently recording sessions
pipeline.index(); // → all completed recordings
pipeline.search({ roomId: "standup", since: Date.now() - 86_400_000 });
pipeline.on("recording:started", (info) => {});
pipeline.on("recording:stopped", (info) => {});
pipeline.on("recording:uploaded", (info) => {});GDPR Article 6 / HIPAA-ready consent collection with optional enforcement.
const { ConsentFlow } = require("webrtc-rooms");
const consent = new ConsentFlow({
server,
required: ["recording"], // kick peers who don't consent within timeout
allParty: true, // all-party consent before recording starts
consentVersion: "v2024-05",
consentTimeoutMs: 30_000,
});
consent.attach();
// Check before starting recording
if (!consent.roomHasConsent("standup", "recording")) {
return "Not all peers have consented to recording";
}
// Grant consent server-side (e.g. consent obtained in your web app)
consent.recordConsent(roomId, peerId, ["recording", "processing"]);
consent.on("consent:granted", (record) => {});
consent.on("consent:withdrawn", ({ peerId, roomId, types }) => {});
consent.on("room:consent:complete", ({ roomId, type }) => {
/* all peers consented */
});Browser sends consent via data relay:
ws.send(
JSON.stringify({
type: "data",
payload: { __consent: "grant", types: ["recording"] },
}),
);
ws.send(
JSON.stringify({
type: "data",
payload: { __consent: "withdraw", types: ["recording"] },
}),
);Region-aware join enforcement and event tagging for GDPR / data locality laws.
const { DataResidency } = require("webrtc-rooms");
const residency = new DataResidency({
server,
localRegion: "ap-south-1",
allowedRegions: ["eu-west-1", "ap-south-1"],
enforceRoomRegion: true,
geoLookup: async (ip) => myGeoIP.lookup(ip).region,
});
residency.attach();
residency.isAllowed("eu-west-1"); // → true
residency.tag({ roomId: "r1" }); // → { roomId: 'r1', __region: 'ap-south-1' }
residency.on("violation", ({ code, roomId, peerRegion }) => {});Data retention record store with legal hold, bulk purge, and subject anonymisation.
const { RetentionPolicy } = require("webrtc-rooms");
const retention = new RetentionPolicy({
recordingRetentionDays: 90,
auditLogRetentionDays: 365,
sessionRetentionDays: 30,
});
// Register a data item for tracking
retention.register({
id: sessionId,
type: "recording",
sub: userId,
meta: { roomId },
});
// Legal hold — prevents purge regardless of retention period
retention.placeLegalHold(userId);
retention.releaseLegalHold(userId);
// Purge expired records
const { purged, held, skipped } = retention.purge();
// GDPR right-to-erasure
retention.anonymise(userId); // → count of records anonymised
// Audit queries
retention.recordsFor(userId);
retention.expiringWithin(7 * 86_400_000); // records expiring in 7 days
retention.on("record:purged", (record) => {});
retention.on("purge:complete", ({ purged, held }) => {});Server-side ECDH public-key distribution for true end-to-end encryption via the browser's Insertable Streams API. Private keys and derived secrets never leave the browser.
const { E2EKeyExchange } = require("webrtc-rooms");
const e2e = new E2EKeyExchange({
server,
requireKeyOnJoin: true, // enforce E2EE — kick peers that don't announce
keyAnnouncementTimeoutMs: 10_000,
allowedCurves: ["P-256", "X25519"],
});
e2e.attach();
e2e.getPeerKey(roomId, peerId); // → { publicKey, curve, version, announcedAt }
e2e.getRoomKeys(roomId); // → [{ peerId, publicKey, curve, version }]
e2e.on("key:announced", ({ peerId, roomId, publicKey, curve }) => {});
e2e.on("key:rotated", ({ peerId, roomId, version }) => {});
e2e.on("key:revoked", ({ peerId, roomId }) => {});Browser-side flow:
// 1. Generate key pair
const kp = await crypto.subtle.generateKey(
{ name: "ECDH", namedCurve: "P-256" },
true,
["deriveKey"],
);
// 2. Export and announce
const raw = await crypto.subtle.exportKey("spki", kp.publicKey);
const b64 = btoa(String.fromCharCode(...new Uint8Array(raw)));
ws.send(
JSON.stringify({
type: "data",
payload: { __e2e: "key:announce", publicKey: b64, curve: "P-256" },
}),
);
// 3. Derive shared secret when you receive another peer's key
const theirKey = await crypto.subtle.importKey(
"spki",
base64ToBuffer(theirPubKey),
{ name: "ECDH", namedCurve: "P-256" },
false,
[],
);
const shared = await crypto.subtle.deriveKey(
{ name: "ECDH", public: theirKey },
kp.privateKey,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"],
);
// 4. Use shared key to encrypt media via Insertable StreamsKey rotation (forward secrecy):
ws.send(
JSON.stringify({
type: "data",
payload: { __e2e: "key:rotate", publicKey: newB64, curve: "P-256" },
}),
);Cross-process signaling bridge via Redis pub/sub. Required for horizontal scaling behind a load balancer.
const { RedisAdapter } = require("webrtc-rooms");
const { createClient } = require("redis");
const pub = await createClient({ url: process.env.REDIS_URL }).connect();
const sub = await createClient({ url: process.env.REDIS_URL }).connect();
const adapter = new RedisAdapter({ pub, sub, server });
await adapter.init();
// Cross-cluster queries
await adapter.getRoomPeers("standup"); // → ['peer-uuid-1', ...]
await adapter.getActiveRooms(); // → ['standup', 'engineering']
await adapter.getRoomPeerDetails("standup"); // → [{ peerId, processId, joinedAt }]Snapshots room structure to Redis and restores it on restart. Peer connections are not persisted (they can't survive a restart), but room IDs and metadata are.
const { RoomPersistence } = require("webrtc-rooms");
const { createClient } = require("redis");
const redis = await createClient({ url: process.env.REDIS_URL }).connect();
const server = createServer({ port: 3000, autoCreateRooms: false });
const persistence = new RoomPersistence({ redis, server });
// Step 1: restore before accepting connections
const { restored, skipped } = await persistence.restore();
console.log(`Restored ${restored.length} rooms`);
// Step 2: persist future changes
persistence.attach();
// Inspect snapshots
const snapshots = await persistence.listSnapshots();
// → [{ roomId, metadata, maxPeers, createdAt, savedAt }]Per-IP connection rate limiting and per-peer signal rate limiting.
const { RateLimiter } = require("webrtc-rooms");
const limiter = new RateLimiter({
maxConnPerMin: 30,
maxMsgPerSec: 50,
maxMsgPerMin: 500,
banDurationMs: 60_000,
whitelist: ["127.0.0.1", "::1"],
});
limiter.attach(server);
limiter.ban("1.2.3.4");
limiter.unban("1.2.3.4");
limiter.bans(); // → [{ ip, expiresIn }]
limiter.destroy();
limiter.on("ip:banned", ({ ip, until }) => {});
limiter.on("connection:blocked", ({ ip }) => {});
limiter.on("signal:blocked", ({ peerId }) => {});HTTP administration REST API — standalone or Express-mountable.
const { AdminAPI } = require("webrtc-rooms");
const admin = new AdminAPI({ server, adminSecret: process.env.ADMIN_SECRET });
admin.listen(4000); // standalone
app.use("/admin", admin.router()); // or mount on Express
await admin.close();| Method | Path | Description |
|---|---|---|
GET |
/admin/health |
Liveness check |
GET |
/admin/stats |
Rooms, peers, memory |
GET/POST |
/admin/rooms |
List / create rooms |
GET/PATCH/DELETE |
/admin/rooms/:id |
Room CRUD |
POST |
/admin/rooms/:id/broadcast |
Send to room |
GET |
/admin/peers |
List peers |
DELETE |
/admin/peers/:id |
Kick peer |
All routes require Authorization: Bearer <adminSecret> when adminSecret is set.
15 additional compliance and observability endpoints.
const { GovernanceEndpoints } = require("webrtc-rooms");
const gov = new GovernanceEndpoints({
server,
adminSecret: process.env.ADMIN_SECRET,
audit,
consent,
residency,
metrics,
tracer,
sessionMgr,
sfuOrchestrator,
threatDetector,
});
gov.listen(4001);| Method | Path | Description |
|---|---|---|
GET |
/audit |
Query audit log |
GET |
/audit/export |
Stream full log as NDJSON |
GET |
/compliance/consents |
All active consent records |
GET/POST/DELETE |
/compliance/consents/:roomId/:peerId |
Consent CRUD |
GET |
/residency |
Data residency status |
GET |
/sessions |
Active session states |
DELETE |
/sessions/:peerId |
Force-expire a session |
GET |
/metrics/prometheus |
Prometheus text export |
GET |
/traces |
Recent trace spans |
GET |
/sfu |
SFU fleet status |
POST |
/sfu/failover |
Trigger manual SFU failover |
GET |
/threats |
Active bans |
DELETE |
/threats/bans/:ip |
Lift a ban |
| Type | Required fields | Description |
|---|---|---|
join |
roomId, metadata? |
Enter (or create) a room |
reconnect |
token, roomId, lastSeenSeq? |
Resume a session |
offer |
target, sdp |
Forward SDP offer |
answer |
target, sdp |
Forward SDP answer |
ice-candidate |
target, candidate |
Forward ICE candidate |
data |
payload, target? |
Relay payload (also used for SFU/E2EE/consent signals) |
metadata |
patch |
Update own metadata |
leave |
— | Voluntarily exit the room |
| Type | Description |
|---|---|
connected |
Peer ID assigned |
session:token |
Reconnect token + TTL (from SessionManager) |
room:joined |
Roster + metadata snapshot |
room:state |
Full snapshot after reconnect |
room:updated |
Room metadata delta |
peer:joined / peer:left / peer:updated / peer:reconnected |
Roster events |
offer / answer / ice-candidate |
Forwarded WebRTC signals |
data |
Relayed application payload |
kicked |
Peer force-closed |
error |
Protocol error { code, message? } |
sfu:ready |
SFU transport info on join |
sfu:published / sfu:subscribed / sfu:layer:auto |
SFU state events |
e2e:key:snapshot / e2e:key:announced / e2e:key:rotated / e2e:key:revoked |
E2EE key events |
consent:required / consent:confirmed |
Consent flow |
region:hint |
Migration hint from RegionRouter |
replay:start / replay:end |
Event replay envelope |
server:warning |
Backpressure / load warnings |
const server = createServer({
port: 3000,
beforeJoin: async (peer, roomId) => {
const user = await db.verifyToken(peer.metadata.token);
if (!user) return "Invalid token";
peer.setMetadata({
token: null, // strip raw token — never broadcast to other peers
userId: user.id,
displayName: user.name,
role: user.role,
});
return true;
},
});For policy-based access control with capability enforcement, use PolicyEngine
instead of a raw beforeJoin hook — it handles token signing, verification,
and capability injection automatically.
const server = createServer({ port: 3000, reconnectTtl: 15_000 });On socket drop the peer's slot is held for 15 seconds. The browser reconnects:
ws.send(
JSON.stringify({
type: "reconnect",
token: sessionStorage.getItem("reconnectToken"),
roomId: currentRoomId,
lastSeenSeq: parseInt(sessionStorage.getItem("lastSeenSeq") || "-1", 10),
}),
);SessionManager provides cross-process resume — the peer can reconnect to a
different process behind a load balancer and still resume their session.
const { RedisAdapter, SessionManager } = require("webrtc-rooms");
const { createClient } = require("redis");
const [pub, sub, rds] = await Promise.all([
createClient({ url: REDIS_URL }).connect(),
createClient({ url: REDIS_URL }).connect(),
createClient({ url: REDIS_URL }).connect(),
]);
// Cross-process signaling
const adapter = new RedisAdapter({ pub, sub, server });
await adapter.init();
// Cross-process session resume
const sessions = new SessionManager({ reconnectTtl: 30_000, redis: rds });
sessions.attach(server);const { RoomPersistence } = require("webrtc-rooms");
const persistence = new RoomPersistence({ redis, server });
await persistence.restore(); // before clients connect
persistence.attach(); // persist future changesconst { NativeSFUEngine, SFUOrchestrator } = require("webrtc-rooms");
const sfu = new NativeSFUEngine({
region: "ap-south-1",
enableSimulcast: true,
});
const orch = new SFUOrchestrator({ server });
orch.register("ap-south-1", sfu);
await orch.init();Browsers use { __sfu: 'publish' } and { __sfu: 'subscribe' } signals
through the data relay. No extra client library required.
const { AdaptiveBitrateController } = require("webrtc-rooms");
const abc = new AdaptiveBitrateController({
sfuEngine: sfu,
mobileFirst: true,
});
abc.attach(server);Browsers send RTCP feedback signals (pli, nack, remb) through the data
relay. The controller automatically adjusts each subscriber's simulcast layer
independently.
const { E2EKeyExchange } = require("webrtc-rooms");
const e2e = new E2EKeyExchange({ server, requireKeyOnJoin: true });
e2e.attach();The server distributes ECDH public keys. Browsers derive shared secrets and encrypt media using the Insertable Streams API. See the E2EKeyExchange section for the full browser-side flow.
const {
MetricsCollector,
Tracer,
AlertManager,
HealthMonitor,
} = require("webrtc-rooms");
const metrics = new MetricsCollector({ server });
const tracer = new Tracer({ server, mode: "buffer" });
const health = new HealthMonitor({ server, metrics });
const alerts = new AlertManager({ channels: [{ type: "console" }] });
metrics.attach();
tracer.attach();
health.attach();
alerts.attachHealthMonitor(health);
// Prometheus
app.get("/metrics", (req, res) =>
res.type("text/plain").send(metrics.toPrometheus()),
);const { ThreatDetector, AuditLogger, PolicyEngine } = require("webrtc-rooms");
const threats = new ThreatDetector({ server });
const audit = new AuditLogger({ server, filePath: "./logs/audit.ndjson" });
const policy = new PolicyEngine({
secret: process.env.POLICY_SECRET,
required: true,
});
threats.attach();
audit.attach();
policy.attach(server);
threats.on("threat", ({ level, threat, ip }) => {
if (level === "ban") alerting.critical(`Banned ${ip} for ${threat}`);
});const { ConsentFlow, DataResidency, RetentionPolicy } = require("webrtc-rooms");
// Consent before recording
const consent = new ConsentFlow({
server,
required: ["recording"],
allParty: true,
});
consent.attach();
// Data locality
const residency = new DataResidency({
server,
localRegion: "ap-south-1",
allowedRegions: ["ap-south-1"],
});
residency.attach();
// Retention and right-to-erasure
const retention = new RetentionPolicy({ recordingRetentionDays: 90 });
retention.register({ id: recordingId, type: "recording", sub: userId });const { ModerationBus } = require("webrtc-rooms");
const modbus = new ModerationBus({ server, policyEngine: policy });
modbus.attach();
// Moderator actions (require 'moderate' capability via PolicyEngine)
modbus.mute({ roomId, targetId: offenderPeerId, actorId: moderatorPeerId });
modbus.lockRoom(roomId, actorId);
// Abuse reporting from browser:
// ws.send(JSON.stringify({ type: 'data', payload: { __mod: 'report:abuse', targetId, reason } }))const http = require("http");
const express = require("express");
const {
createServer,
AdminAPI,
RateLimiter,
MetricsCollector,
GovernanceEndpoints,
} = require("webrtc-rooms");
const app = express();
const httpServer = http.createServer(app);
const server = createServer({ server: httpServer });
new RateLimiter({ maxConnPerMin: 30 }).attach(server);
const metrics = new MetricsCollector({ server });
metrics.attach();
const admin = new AdminAPI({ server, adminSecret: process.env.ADMIN_SECRET });
app.use("/admin", admin.router());
app.get("/metrics", (req, res) =>
res.type("text/plain").send(metrics.toPrometheus()),
);
httpServer.listen(3000);Full type definitions included for all 36 exports:
import {
createServer,
SignalingServer,
Room,
Peer,
SessionManager,
PolicyEngine,
NativeSFUEngine,
SFUOrchestrator,
AdaptiveBitrateController,
RegionRouter,
ThreatDetector,
AuditLogger,
MetricsCollector,
Tracer,
AlertManager,
HealthMonitor,
EventReplay,
BackpressureController,
ModerationBus,
RecordingPipeline,
ConsentFlow,
DataResidency,
RetentionPolicy,
E2EKeyExchange,
RedisAdapter,
RoomPersistence,
RateLimiter,
AdminAPI,
GovernanceEndpoints,
// Type aliases
PeerStateValue,
MetadataMap,
BeforeJoinHook,
ServerMessage,
ClientSignal,
ServerStats,
RoomSnapshot,
Session,
Policy,
Capability,
SFUHealthValue,
LoadLevelValue,
ConsentType,
PersistedRoomSnapshot,
RestoreResult,
PublicKeyEntry,
RoomKeyEntry,
} from "webrtc-rooms";
const server: SignalingServer = createServer({ port: 3000 });
server.on("peer:joined", (peer: Peer, room: Room) => {
console.log(peer.metadata.displayName, peer.state);
});npm install
npm test
node tests/index.test.js # core signaling — 43 tests
node tests/features.test.js # 1.x features — 41 tests
node tests/redis-persistence-e2e.test.js # Redis + E2EE — 35 tests
node tests/test1.test.js # Test 1 modules — 62 tests
node tests/test2.test.js # Test 2 modules — 55 tests
node tests/test3.test.js # Test 3 + CLI + load — 29 testsAll suites use in-process mocks. No live Redis, no SFU server, no external
test runner required. Run directly with node.
See CONTRIBUTING.md for setup instructions, coding standards, pull request process, and code merge rules.
For project governance, see GOVERNANCE.md. For security reports, see SECURITY.md. For community standards, see CODE_OF_CONDUCT.md. For support channels, see SUPPORT.md.
- Redis pub/sub adapter for multi-process deployments
- Room persistence across restarts
- End-to-end encryption key-exchange
- Native SFU (NativeSFUEngine) — zero external SFU dependency
- Multi-region SFU orchestration and failover
- Adaptive bitrate with simulcast layer control
- Enterprise compliance toolkit (ConsentFlow, DataResidency, RetentionPolicy)
- Production observability (MetricsCollector, Tracer, AlertManager, HealthMonitor)
- Developer CLI (init, simulate, benchmark, health)
-
webrtc-rooms-client— browser SDK with auto-reconnect and typed signaling