Skip to content

About

a serverless, Zoom-like video conferencing web app

Topics

Resources

Stars

42 stars

Watchers

0 watching

Forks

Repository files navigation

Rendezvous

where conversations meet, serverlessly.

A serverless, Zoom-like video conferencing web app,
built with React, TypeScript, MUI, and PeerJS on top of WebRTC.

Discord License Build Status Deployment Status

Deutsch · English · Español · Français · 日本語 · 한국어 · Português · Русский · 中文


👉 Try it online: https://www.predatorray.me/rendezvous/

Meeting Screenshot Meeting Screenshot on Mobile

There is no application server — the host of each meeting acts as a relay hub for chat messages and media streams, so each participant only maintains connections to the host instead of every other participant. The PeerJS public broker is used only for the initial WebRTC signaling.

About the name

Rendezvous is named after the Rendezvous Lodge atop Blackcomb Mountain in Whistler Village — the spot where the author meets up with skier friends.

Features

  • Pick a name, host a meeting, or join an existing one by code or link
  • 6-letter human-readable meeting codes (~300M combinations)
  • Tile-based video grid with auto-layout
  • Tile shows the participant’s initials when their camera is off
  • Mute / unmute audio, start / stop video (mute icon shown on the tile)
  • Collapsible right-side chat drawer with timestamps and join/leave notices
  • Chat history is preserved by the host so late joiners see prior messages
  • Sharable invite link and copy-able meeting code
  • Host leaving ends the meeting for everyone
  • No accounts, no passcodes, fully static-site deployable

Tech stack

  • React 19 + TypeScript (Create React App)
  • MUI v7 (dark, minimalist Zoom-inspired theme)
  • React Router v7 (HashRouter for static hosting)
  • PeerJS for signaling and WebRTC orchestration
  • gh-pages for GitHub Pages deployment

Running locally

npm install
npm start

Open http://localhost:3000. To test multi-party meetings open additional incognito windows and use the same meeting code.

Building

npm run build

Outputs a static bundle in build/ ready to be served from any CDN. The app uses HashRouter, so it works on hosts that don’t support client-side SPA rewrites (e.g. GitHub Pages).

Deploying to GitHub Pages

  1. Add a homepage field to package.json pointing at your Pages URL:

    "homepage": "https://YOUR_USER.github.io/rendezvous"
    
  2. Push to GitHub, then run:

    npm run deploy
    

    This builds and pushes the build/ directory to the gh-pages branch using gh-pages. Enable Pages from the gh-pages branch in repo Settings → Pages.

Architecture

  • src/peer/MeetingClient.ts — owns the PeerJS Peer and implements both host (relay) and client behaviors.
  • src/peer/useMeeting.ts — React hook that adapts the meeting client to component state.
  • src/types.ts — shared types and the wire protocol carried over PeerJS DataConnections.
  • src/pages/ — Home and Meeting pages.
  • src/components/ — VideoGrid, VideoTile, ChatDrawer, Controls, ShareDialog.

Wire protocol

Messages exchanged over the data connection between a client and the host:

Type Direction Purpose
hello client → host Sent on connect with the participant’s name
welcome host → client Returns assigned id, roster, and timeline
roster host → all Updated member list (joins, leaves, state)
chat-send client → host New chat message draft
timeline host → all Authoritative chat or system event
state client → host Participant changed audio/video
end host → all Host is leaving — meeting is over

Media topology

Each participant places exactly one outbound media call to the host carrying their own stream. The host accepts and:

  1. Calls every other connected client with that incoming stream, tagged with metadata.peerId so the receiver knows which participant it represents.
  2. Pushes its own stream and every existing remote stream to a new client when they join.

This gives every client a constant number of signalling sessions with the host (one data connection + N media connections), avoiding the classic O(N²) mesh.

flowchart LR
    A[Client A] -- stream A --> H((Host))
    B[Client B] -- stream B --> H
    C[Client C] -- stream C --> H
    H -- streams B, C --> A
    H -- streams A, C --> B
    H -- streams A, B --> C
Loading

Limitations / caveats

  • The host’s upstream bandwidth bounds meeting size (relay is on a consumer-grade browser tab).

  • Forwarding remote tracks through the host re-encodes them; quality is limited to what getUserMedia and the browser’s WebRTC stack negotiate.

  • Default PeerJS broker is used; for production you can host your own PeerServer and pass it to the Peer constructor.

  • The "serverless" property only holds when every participant can establish a direct peer-to-peer connection (host candidates, or server-reflexive candidates obtained via STUN for endpoints behind cone NATs). If any participant sits behind a symmetric NAT, ICE cannot negotiate a direct path, and media/data are relayed through a TURN server — meaning that traffic is proxied by a third-party server rather than flowing directly between peers.

  • If connection support from both Metered and Turnix is unavailable, the app warns participants to try another network (such as home Wi-Fi) and disconnect their VPN if possible, then rejoin. Direct connections can still work. The app checks credentials and runs a background relay-only ICE check, closing it on success or after 10 seconds. This sends no media and does not delay joining. A failed check indicates that relay support could not be reached from this browser; it does not identify the cause. Metered's quota documentation explains that quota enforcement can disable credentials, while Get Credential documents no quota-specific HTTP error. A successful credentials response alone is therefore insufficient to confirm relay availability.

TURN providers and credential refresh

Production supplies WebRTC with both Metered and Turnix ICE servers. Each list is loaded independently; a failed provider does not discard the other provider's servers. The relay availability check tests the combined list, so a working provider prevents the connectivity warning.

Metered credentials are fetched at runtime using METERED_API_KEY (as before). Turnix blocks cross-origin browser API calls. Instead, npm run build first runs scripts/generate-turnix-credentials.cjs to request seven-day credentials and write public/turnix-ice-servers.json. This generated file is ignored by Git. Only expiring TURN usernames/passwords are published; the Turnix API token is never included in the browser bundle or credential file. Public TURN credentials can still be used by others to consume the account's allowance.

Configure repository Settings → Secrets and variables → Actions → New repository secret with name TURNIX_API_TOKEN and your Turnix API token as its value. Both production and PR preview builds use this secret. For local production builds, provide TURNIX_API_TOKEN through your environment. Do not prefix it with REACT_APP_ or commit it to an environment file.

The production publish workflow runs daily at 04:23 UTC to refresh the seven-day credentials, as well as on pushes to main and manual dispatch. Monitor workflow warnings: GitHub schedules can be delayed or disabled after repository inactivity. Missing secrets produce an empty Turnix file; API failures (including quota HTTP 429) produce a workflow warning and an empty file, allowing Metered to continue. The API token and upstream response bodies are never logged. Browsers reject expired Turnix files and request them without caching. Existing tabs must reload before their loaded credentials expire to use Turnix for new relay allocations. PR previews refresh when rebuilt and expire after seven days without another build.

Turnix API references: credentials, usage limits. The seven-day TTL was verified against the live API; relay connectivity still depends on network access.

About

a serverless, Zoom-like video conferencing web app

Topics

Resources

Stars

42 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages