Eyebridge — How It Works
End-to-end guide to the video calling pipeline
Introduction
Eyebridge is a browser-native, 1-to-1 video calling application built on WebRTC — the open standard that lets two browsers exchange real-time audio and video without a media relay server. No account, no plugin, no download. A short meeting code is all that is needed to connect two peers anywhere in the world.
This page explains exactly how Eyebridge works under the hood — from the moment a user clicks New meeting to the point where P2P media is flowing. It is aimed at developers who want to understand the WebRTC handshake, the signaling architecture, and the session state model. Each section builds on the last, so reading top-to-bottom gives the clearest picture.
High-Level Architecture
Browser A
Creator
Signal Server
Next.js API + MongoDB
STUN Server
stun.l.google.com
Browser B
Joiner
Once connected, media flows directly peer-to-peer — the signal server is no longer involved.
The signal server — a Next.js API route backed by MongoDB — acts only as a temporary message bus during setup. It ferries SDP offers, SDP answers, and ICE candidates between the two browsers. A Google STUN server helps each peer discover its public IP address and port so they can reach each other through NAT.
Once the WebRTC handshake completes and the connection state reaches connected, all audio and video travel directly between the two browsers. The signal server is no longer in the media path and receives no further traffic for that session.
WebRTC Fundamentals
WebRTC (Web Real-Time Communication) is a browser API that lets two peers exchange audio, video, and data without a relay server. The core object is RTCPeerConnection — one instance per browser tab. It manages everything from codec negotiation to packet loss recovery.
SDP (Session Description Protocol) is a text format that describes what a peer can send and receive: codecs, resolutions, bitrates, and network addresses. The Creator calls createOffer() to produce an SDP offer; the Joiner responds with createAnswer(). Eyebridge prefers the VP9 codec when available — it delivers better quality at lower bitrates than VP8. Video is capped at 1.5 Mbps and audio at 128 kbps to keep calls affordable on mobile data.
ICE (Interactive Connectivity Establishment) is the process of finding a network path between two peers. Each browser collects ICE candidates — host addresses, reflexive addresses discovered via STUN, and relay addresses via TURN. Eyebridge uses only a Google STUN server (stun.l.google.com:19302) because both peers are expected to be reachable after NAT traversal; no paid TURN relay is needed. Candidates are exchanged over the signal server and fed into addIceCandidate() on the remote peer.
Signaling Flow
- 1Both peers call getUserMedia() to acquire their local camera and microphone tracks before any network activity begins.
- 2The Creator creates an RTCPeerConnection, generates an SDP offer, and stores it as its local description.
- 3The offer is posted to the signal server — a Next.js API route that writes it into MongoDB.
- 4The Joiner polls the signal server and receives the offer. Polling fires every second until a message arrives.
- 5The Joiner sets the offer as its remote description, then generates an SDP answer and stores it as its own local description.
- 6The answer is posted to the signal server the same way the offer was.
- 7The Creator polls and receives the answer.
- 8The Creator applies the answer as its remote description. Both sides now agree on codecs, bitrates, and media capabilities.
- 9As each peer collects ICE candidates, it posts them to the signal server. The other peer polls and feeds them into addIceCandidate().
- 10Once a viable candidate pair is found, the connection state transitions to CONNECTED. Polling stops and media flows directly peer-to-peer — the signal server receives no further traffic.
Signaling Modes
Long-Poll
Default- –Polls every 1 second until a message arrives
- –Messages stored in MongoDB with 2-min TTL
- –Works with zero extra configuration
- –Higher latency than WebSocket (~1 s worst case)
- –Compatible with any hosting platform
Pusher WebSocket
Lower latency- –Real-time push — no polling during signaling
- –Requires 4 env vars (key, secret, app ID, cluster)
- –Catch-up poll fires once on subscribe to grab any missed messages
- –Handshake completes noticeably faster on slow networks
- –Pusher free tier: 200k messages/day, 100 concurrent connections
How the app picks: if the environment variable NEXT_PUBLIC_PUSHER_KEY is set, Eyebridge uses Pusher WebSocket for signaling. If it is absent, it falls back to long-poll automatically — no code change required.
Both modes produce the same WebRTC handshake — only the transport layer differs. Pusher is the better choice for production deployments where handshake latency matters; long-poll is the right choice when you want zero external dependencies or are developing locally without a Pusher account.
Session Lifecycle
| State | Meaning | Triggered by |
|---|---|---|
waiting | Session created; no Joiner yet | Creator clicks New meeting |
active | Both peers are connected; media is flowing | Joiner enters the meeting code |
ended | Call finished; session is closed to new joiners | Creator clicks End for all, or 10-min timer fires |
expired | No one joined before the TTL elapsed | MongoDB TTL index on expiresAt (10 min from creation) |
Media & Controls
Once connected, both peers interact with the call through a set of controls. Each control maps to a specific browser API call or server action.
audioTrack.enabled = falseSilences outbound audio without stopping the track — the microphone stays acquired so unmuting is instant.
videoTrack.enabled = falseHides video locally. On mobile, the track is also fully stopped to release the camera indicator light.
sender.replaceTrack(newTrack)Lists available video devices via enumerateDevices(), opens a new stream from the chosen camera, and replaces the sender's track in-place — no SDP renegotiation needed.
setSinkId()Routes audio output to the earpiece (near ear) or the loudspeaker. Uses setSinkId() on desktop; reassigns the audio element's srcObject on mobile where setSinkId() is not available.
draggable overlayMoves the remote video feed into a small draggable overlay so both peers can multitask without leaving the call tab.
peerConnection.close()The local peer closes its RTCPeerConnection and navigates away. The session status remains active — the other peer can invite a new participant to re-join with the same code.
PATCH /sessions/[id]Only the Creator can end the call for both parties. A PATCH request sets the session status to ended, which blocks any future join attempts for that meeting code.
Session Limits & Cleanup
| Item | Limit | Enforcement |
|---|---|---|
| Meeting duration | 10 min | Server-side expiresAt field + client countdown timer |
| Signal TTL | 2 min | MongoDB TTL index on eyebridge_signals.createdAt |
| Session TTL | 10 min | MongoDB TTL index on eyebridge_sessions.expiresAt |
These limits exist because Eyebridge relies on a free Google STUN server with no paid TURN relay, a MongoDB Atlas free tier for storage, and standard Pusher free quotas. Keeping sessions short and cleaning up stale signals prevents unbounded storage growth and ensures the service stays within free-tier limits indefinitely.