Small browser-to-browser WebRTC group chat with audio, video, and text.
The app uses a lightweight signaling server and mesh WebRTC. It is meant for learning, demos, and small-room testing, not large production meetings.
Live demo:
https://chat-p2p.hcharlie.deno.net/
- Create a room with a 6-character code.
- Join the same room from up to 5 browsers/devices.
- Audio and video are sent peer-to-peer with WebRTC.
- Text chat is sent through the WebSocket signaling server.
- Google STUN is used by default.
- Optional TURN support is configured with environment variables.
The server does not relay audio or video. It only:
- Serves the static web app.
- Keeps in-memory room state.
- Assigns each browser a peer ID.
- Relays WebRTC signaling messages between peers.
- Broadcasts text chat messages to the room.
For media, the browsers use mesh WebRTC. In a 5-person room, each browser creates 4 WebRTC peer connections and sends its camera/microphone stream to each other browser.
- Deno
- A modern browser
cloudflared, only if testing across the public internet with a temporary tunnel
Install Deno:
brew install deno
deno --versionInstall Cloudflare Tunnel:
brew install cloudflared
cloudflared --versionStart the app:
deno task startOpen:
http://localhost:8000
To test locally, open that URL in multiple browser windows or tabs.
Camera and microphone access require HTTPS outside localhost. A free Cloudflare quick tunnel is the easiest way to test with another laptop or someone outside your network.
Terminal 1:
deno task startTerminal 2:
cloudflared tunnel --url http://localhost:8000cloudflared prints a temporary public URL like:
https://example-random-name.trycloudflare.com
Test flow:
- Send the
https://...trycloudflare.comURL to each user. - User A opens the URL and clicks
Create Room. - User A is already inside the room.
- User A copies the room code from the call header.
- Other users open the same URL, enter the code, and click
Join Room. - The room supports up to 5 users total.
Keep both terminal processes running while testing. If either the Deno server or Cloudflare tunnel stops, the public URL stops working.
The creator does not need to join with the code. Clicking Create Room creates the room and joins it immediately.
The room code is only for other users. If the creator opens another tab and joins with the same code, that tab counts as another participant.
Rooms are stored in memory. Restarting the server clears all rooms.
Default free STUN config:
STUN_URLS=stun:stun.l.google.com:19302STUN helps peers discover public network paths. It is enough for many home and mobile networks.
TURN is needed when direct peer-to-peer connections are blocked by strict NATs or firewalls. TURN relays media through a server, so it usually costs bandwidth.
Optional TURN config:
TURN_URLS=turn:your-turn-host:3478,turns:your-turn-host:5349
TURN_USERNAME=your-username
TURN_CREDENTIAL=your-password
deno task startTURN credentials are sent to the browser because WebRTC needs them. For production, prefer short-lived TURN credentials instead of a permanent shared password.
Deploy this as a single web service that supports HTTP and WebSocket upgrades.
This repo includes Deno Deploy config in deno.json:
{
"deploy": {
"runtime": {
"type": "dynamic",
"entrypoint": "server.ts"
}
}
}The entrypoint must be server.ts. Do not use server.js on Deno Deploy; that file is a Node fallback and uses CommonJS require().
Recommended Deno Deploy app settings:
Framework preset: No Preset
Install command: empty
Build command: empty
Runtime mode: Dynamic
Entrypoint: server.ts
Arguments: empty
Working directory: .
Environment variables are optional for the default Google STUN setup. The app falls back to stun:stun.l.google.com:19302 automatically.
After deploy, verify:
https://chat-p2p.hcharlie.deno.net/config
Expected response:
{"iceServers":[{"urls":["stun:stun.l.google.com:19302"]}]}Then open the app URL, click Create Room, and confirm the call screen appears with a room code in the header.
Run command:
deno run --allow-net --allow-read --allow-env server.tsEnvironment:
PORT=8000
STUN_URLS=stun:stun.l.google.com:19302The browser automatically uses:
/for the app/wsfor WebSocket signaling/configfor STUN/TURN config
In production, serve the app over HTTPS so browsers allow camera and microphone access.
- Add a real room lobby with display names instead of short peer IDs.
- Add explicit join/leave state so stale tabs are easier to understand.
- Show per-peer connection status in each video tile.
- Add screen sharing.
- Add mute/camera state indicators for remote users.
- Add better error messages for blocked camera/microphone permissions.
- Add basic automated browser tests for create/join/signaling flows.
- Add TURN for networks where direct peer-to-peer fails.
- Prefer short-lived TURN credentials instead of static credentials.
- Add room expiration and cleanup timers.
- Add reconnect handling for temporary WebSocket disconnects.
- Add rate limits and input validation around room creation and chat.
This app currently uses mesh WebRTC. Mesh is simple, but every user sends media to every other user. That becomes expensive quickly.
For production group video or live streaming, move to an SFU architecture:
User A -> SFU -> Users B/C/D/E
User B -> SFU -> Users A/C/D/E
An SFU lets each user upload media once, then the server forwards streams to the right subscribers. This is the normal architecture for larger group calls, live classes, webinars, and production streaming.
Good SFU options:
- LiveKit
- mediasoup
- Janus
- Jitsi
- Pion-based custom SFU
LiveKit is a strong next step because it provides the SFU, client SDKs, room management, subscriptions, permissions, recording options, and deployment paths. Moving to LiveKit would replace most of this app's custom WebRTC mesh/signaling code with LiveKit rooms and SDK calls.
Each room supports up to 5 peers. Room is full means the signaling server already has five WebSocket connections registered for that room.
Common causes:
- The room is already open in another tab, window, or device.
- A join button was clicked more than once.
- A laptop refreshed and the old WebSocket has not closed yet.
- A stale test room is still stored in the running server process.
Clean reset:
- Close all app tabs on all devices.
- Stop the Deno server with
Ctrl+C. - Stop the Cloudflare tunnel with
Ctrl+C. - Start the Deno server again.
- Start a new Cloudflare tunnel.
- Use the new
https://...trycloudflare.comURL.
If users can create/join a room but the badge never becomes Connected, WebRTC likely failed on that network. Add a TURN server and set TURN_URLS, TURN_USERNAME, and TURN_CREDENTIAL.
Mesh WebRTC is simple, but it does not scale well. With 5 users, each user uploads media to 4 other users. CPU and bandwidth usage grow quickly as the room gets larger.
For larger or production group calls, use an SFU such as LiveKit, mediasoup, Janus, or Jitsi.