Skip to main content

socket.server

socket.server: {
emit(event: string, ...args: any[]): this;
on(event: string, handler: (...args) => void): this;
off(event: string, handler: (...args) => void): this;
}

socket.server is rtc.io's escape hatch. Anything you emit here goes straight through socket.io to the signaling server — same as if you'd written plain socket.io-client code. Anything you listen to here fires for events the server pushes back.

This is the right surface for:

  • Room/lobby managementjoin-room, leave-room, presence broadcasts the server fans out.
  • Auth handshakes that need server validation.
  • ICE-credential vending (ask-ice → server returns short-lived TURN creds).
  • App-level events that should funnel through your server, not peer-to-peer.

It is not the right surface for:

  • Chat, RPC, app data — use socket.emit (peer-to-peer ctrl channel) or a custom channel.
  • Streams — use socket.emit('camera', stream) so the library wires up transceivers.

Why the escape hatch exists

socket.emit is overridden in rtc.io's Socket to route over DataChannels by default. For application events that should go through the server, you need a way to reach the underlying socket.io emitter without rtc.io's DataChannel routing. That's socket.server.

Internally it's just bound copies of the parent class's emit/on/off:

rtc.ts (excerpt)
this._rawEmit = (ev, ...args) => super.emit(ev, ...args);
this._rawOn = (ev, h) => super.on(ev as any, h);
this._rawOff = (ev, h) => super.off(ev as any, h);

get server() {
return {
emit: (ev, ...args) => { this._rawEmit(ev, ...args); return this; },
on: (ev, h) => { this._rawOn(ev, h); return this; },
off: (ev, h) => { this._rawOff(ev, h); return this; },
};
}

So socket.server.emit("join-room", ...) is super.emit("join-room", ...) — vanilla socket.io.

Usage examples

join-room

// Client
socket.server.emit("join-room", { roomId: "stand-up", name: "alice" });
socket.server.on("user-connected", ({ id, name }) => roster.add(id, name));
socket.server.on("user-disconnected", ({ id }) => roster.remove(id));

// Server
socket.on("join-room", ({ roomId, name }) => {
socket.data.name = name;
socket.join(roomId);
// Tell every existing peer in the room to initiate an offer to the new one.
socket.to(roomId).emit("user-connected", { id: socket.id, name });
socket.to(roomId).emit("#rtcio:init-offer", { source: socket.id });
});

#rtcio:init-offer is also emitted on server.on(...), but you generally don't need to listen for it — rtc.io's internal listener handles it.

Authentication

Pass a token in the auth payload (socket.io standard); validate server-side before letting the connection proceed:

// Client
const socket = io(URL, {
iceServers: [...],
auth: { token: jwt },
});

// Server
io.use((socket, next) => {
const ok = verifyJwt(socket.handshake.auth.token);
if (!ok) return next(new Error("unauthorized"));
socket.data.userId = ok.sub;
next();
});

The middleware runs before connect fires. Failed auth shows up on the client as connect_error.

Server-mediated events

If you want a "report this user" feature, the server is the right place to log it (the user shouldn't trust other peers to do it):

// Client
socket.server.emit("report", { peerId, reason });

// Server
socket.on("report", async ({ peerId, reason }) => {
await db.reports.insert({ from: socket.data.userId, target: peerId, reason });
});

ICE credential vending

Don't put long-lived TURN credentials in the client bundle. Mint them on demand:

// Server
import crypto from "node:crypto";

socket.on("ask-ice", (cb) => {
const ttl = 600; // 10 min
const username = String(Math.floor(Date.now() / 1000) + ttl);
const credential = crypto.createHmac("sha1", TURN_SECRET).update(username).digest("base64");
cb({
iceServers: [
{ urls: "stun:turn.example.com:3478" },
{ urls: "turn:turn.example.com:3478", username, credential },
],
});
});

// Client
const { iceServers } = await new Promise((res) =>
socket.server.emit("ask-ice", res)
);
const socket = io(URL, { iceServers });

Note that the ack callback (the second argument to socket.server.emit) does work here — socket.server is plain socket.io, which supports acks.

What goes through socket.server

Anything your app explicitly opts into. The server has no default handlers from rtc.io's side — you wire up exactly the events you want. The library only auto-handles #rtcio:message and #rtcio:init-offer; everything else is your code.

If you don't need application-level routing through the server, don't write any. The peer-to-peer DataChannel covers most use cases.