Architecture
How this guide should be used.
Step 01
Use realtime for signaling only
DomVia Realtime should coordinate call state and WebRTC negotiation. The actual microphone, camera, and screen media should flow through WebRTC, not through normal realtime events.Step 02
Keep call rooms protected
Use private or presence channels for calls. Your trusted server should verify the signed-in user belongs to the chat room, trip, support room, or delivery session before signing access.Step 03
Start one-to-one before group calls
One-to-one WebRTC calls can start with peer-to-peer plus TURN fallback. Group calls should later use an SFU so every user does not have to stream to every other user.Step 04
Separate call state from chat messages
Chat messages stay in @domvia/realtime-chat. Call sessions stay in @domvia/realtime-calls. A chat room can start a call, but the signaling layer should remain separate.Step 05
Encrypt signaling when the room needs stronger privacy
WebRTC encrypts media in transit. Strong privacy mode can also encrypt sensitive offer, answer, ICE, and media-state JSON before those signaling events move through realtime.Examples
Focused implementation notes.
Client call session
Join a protected call signaling channel, listen for WebRTC negotiation events, and send call state updates when client events are enabled.
import { DomViaRealtime } from "@domvia/realtime";
import {
buildCallChannelName,
createCallSession,
} from "@domvia/realtime-calls";
const realtime = DomViaRealtime.create({
key: "pk_live_your_public_key",
host: "ws.domvia.net",
authEndpoint: "/realtime/auth",
clientEvents: true,
});
const channelName = buildCallChannelName({
channelKind: "private",
scope: "room",
scopeId: "chat-room-42",
sessionId: "call-9001",
});
const call = createCallSession(realtime, {
callId: "call-9001",
channelName,
});
await call.waitUntilReady();
call.onOffer(async (event) => {
await peerConnection.setRemoteDescription({
type: "offer",
sdp: event.sdp,
});
});
call.onAnswer(async (event) => {
await peerConnection.setRemoteDescription({
type: "answer",
sdp: event.sdp,
});
});
call.onIceCandidate(async (event) => {
await peerConnection.addIceCandidate({
candidate: event.candidate,
sdpMid: event.sdp_mid,
sdpMLineIndex: event.sdp_mline_index,
});
});
call.sendRinging({
userId: "user-2",
});Trusted server authorization
A private call channel should be signed only after your server confirms the user can access the room or call session.
import { authorizePrivateChannel } from "@domvia/realtime-server";
app.post("/realtime/auth", async (request, response) => {
const user = await requireSignedInUser(request);
const channelName = request.body.channel_name;
const socketId = request.body.socket_id;
if (!channelName.startsWith("private-call.room.")) {
return response.status(403).json({ message: "Forbidden" });
}
const roomId = extractRoomIdFromCallChannel(channelName);
if (!await userCanAccessRoom(user.id, roomId)) {
return response.status(403).json({ message: "Forbidden" });
}
return response.json(
await authorizePrivateChannel({
key: process.env.DOMVIA_REALTIME_KEY,
secret: process.env.DOMVIA_REALTIME_SECRET,
socketId,
channelName,
}),
);
});Where WebRTC fits
Realtime sends signaling messages. WebRTC uses those messages to establish the media path.
// DomVia Realtime signaling: call.started call.ringing call.accepted call.offer call.answer call.ice_candidate call.ended // WebRTC media: microphone audio camera video screen sharing // Production media reliability: STUN for discovery TURN for fallback SFU later for group calls
Safety
Rules before production traffic.
Do not send raw audio or video through realtime events.
Use WebRTC for media and DomVia Realtime for signaling.
Protect every call channel with trusted server authorization.
Use strong privacy mode for sensitive call signaling payloads when needed.
Use TURN fallback for production one-to-one calls.
Use an SFU later for serious group calls.
Rate-limit noisy call signaling events like ICE candidates.
Keep call logs and missed-call records in your trusted server database.
Continue