- TypeScript 47%
- Svelte 21.4%
- Rust 12.8%
- Shell 7.6%
- CSS 7.6%
- Other 3.5%
| backend | ||
| docs | ||
| frontend | ||
| infra | ||
| mk | ||
| scripts | ||
| .dockerignore | ||
| .gitignore | ||
| AGENTS.md | ||
| backlog-methodology.md | ||
| current-hypotheses.md | ||
| Dockerfile | ||
| failed-approaches.md | ||
| README.md | ||
| todo.md | ||
Web 1:1 video chat
Supported browser matrix
- Primary supported mobile call path: latest iOS Safari
<->latest Android Chromium. - Supported mobile background-media target: Android Chromium.
- iOS Safari: supported for the core call flow, but Media Session notification / lock-screen behavior remains best-effort browser behavior rather than an app-level promise.
- Firefox Android: best-effort foreground calling only. Do not treat persistent media notification, durable background microphone capture, or long-running background call behavior as supported product features.
The current backlog and cleanup plan for this support boundary lives in docs/unaddressed/notes/01-supported-mobile-scope-and-firefox-android-cleanup.md.
First multi-party release policy
- The first multi-party slice is tuned for up to
4total participants on desktop. - Mobile multi-party is best-effort and should stay at
3total participants or fewer. - The current UX is staged-first: one remote participant occupies the main stage, the rest stay in the participant roster, and screen share takes precedence for the staged participant.
- The current remote media path is also staged-first: remote audio follows the staged participant instead of mixing every remote participant simultaneously.
- LiveKit
adaptiveStreamanddynacastremain enabled, and the existing local camera publish plan stays on the currenth540/h720simulcast ladder. As rooms grow, quality may drop tobalanced,constrained, oraudio-priority; HD for every participant is not a product promise. - Rooms beyond the first-release cap are unsupported/best-effort only until a later note adds an explicit expansion or enforcement strategy.
Single-VPS deployment
This repo builds a mobile-friendly, link-based WebRTC chat where one Rust container serves the SPA, signaling API, room/token endpoints, and an embedded turn-rs relay on the same VPS. Nginx only terminates HTTPS for the web app and websocket traffic.
1. Prerequisites
- One Ubuntu 24.04+ VPS (minimum 2 vCPU, 4 GiB RAM) with ports
22,80,443,3478/udp, and3478/tcpopen in your firewall. - One DNS A record pointing your chat domain (for example
meet.example.com) to the VPS IP. - Docker, Node 20+, Rust 1.83+ toolchain, and Certbot with the
nginxplugin installed.
2. Obtain TLS certificates via Certbot
sudo certbot --nginx -d meet.example.com
Certbot writes /etc/letsencrypt/live/meet.example.com/{fullchain.pem,privkey.pem} and nginx reuses those files directly.
3. Build the frontend + backend
cd frontend
npm ci
npm run build
frontend/scripts/postbuild.mjs copies the Svelte build output into frontend/public and backend/public so Axum can serve the SPA from the Rust binary.
cd ../backend
cargo build --release
4. Configure and launch the container
The repo already includes ./mk/run and ./mk/deploy-single-vps. For a single-VPS deployment, the container publishes:
- your HTTP app port, for example
8011 -> 8000 - embedded TURN on
3478/udp - optional TURN-over-TCP on
3478/tcp(enabled by default)
If you prefer a systemd wrapper around ./mk/run, use something like:
[Unit]
Description=catchup single-vps app
After=network.target
[Service]
WorkingDirectory=/home/ubuntu/src/repos/video-chat
ExecStart=/home/ubuntu/src/repos/video-chat/mk/run 8011 single 0
Environment=TURN_SECRET=<hex from openssl rand -hex 32>
Environment=CATCHUP_RELAY_PORT=3478
Environment=CATCHUP_TURN_ENABLE_TCP=1
Restart=on-failure
[Install]
WantedBy=multi-user.target
Reload systemd and start the service:
sudo systemctl daemon-reload
sudo systemctl enable --now catchup
4.1 Runtime environment variables
TURN_SECRET: required HMAC key used by the embedded relay for TURN-style credentials. Generate it once withopenssl rand -hex 32. The backend now fails closed at startup if this is missing or blank. Do not commit it.CATCHUP_RELAY_PORT: TURN bind port inside the container. Default3478.CATCHUP_TURN_ENABLE_TCP: advertise and serve TURN over TCP on the same port. Default1.TURN_HOSTS: optional comma-separated TURN hostnames advertised to the browser. For single-VPS deploys, leaving this unset is fine because the backend derives the host fromCATCHUP_DOMAIN.STUN_SERVERS: optional comma-separated STUN URLs. Defaults to empty so the app stays self-sovereign and uses only the embedded relay unless you explicitly opt into extra STUN infrastructure.
4.2 First-release abuse throttling
- The backend now applies a fixed
60second abuse-throttle window on the sensitive issuance endpoints. - Current first-release limits are:
/create-room:8requests per source IP per60seconds/api/livekit/token:30requests per source IP per60seconds/turn-cred:30requests per source IP per60seconds
- Source identification is direct-socket-IP by default. If the direct peer is loopback/private/unique-local, the backend will trust the first valid
X-Forwarded-ForIP instead so a local reverse proxy can preserve client identity. - If you deploy behind a proxy or ingress layer, ensure it preserves client IP information correctly. Otherwise multiple clients may collapse onto the proxy source and share the same throttle bucket.
- These limits are deliberately simple first-release abuse controls, not a full DDoS mitigation or quota system.
5. Deploy nginx for meet.example.com
You only need one nginx config from this repo:
infra/turn.nginxfor HTTPS web traffic to127.0.0.1:8011
5.1 HTTP reverse proxy
Copy infra/turn.nginx into /etc/nginx/sites-available/meet.example.com and symlink it into /etc/nginx/sites-enabled. Update the upstream 127.0.0.1:8011 if you deploy to a different host port.
That file does the following:
- Redirects all HTTP → HTTPS.
- Proxies all HTTP requests, including static assets, websocket upgrades, and API endpoints, to the catchup container.
- Adds HSTS headers and sane timeouts for long-lived signaling.
After installing the HTTP proxy file:
sudo nginx -t
sudo systemctl reload nginx
Verify from the VPS:
curl -I https://meet.example.com/healthz
nc -vu meet.example.com 3478
nc -vz meet.example.com 3478
6. Firewall and operations
sudo ufw allow 22
sudo ufw allow 80
sudo ufw allow 443
sudo ufw allow 3478/udp
sudo ufw allow 3478/tcp
Renew certs with sudo certbot renew and reload nginx after renewal. If you change stream includes, always run sudo nginx -t before reloading.
7. Deploy with the existing helper
Your current deploy command stays the same:
./mk/deploy-single-vps myvpshostname 8011 0 0
That copies the repo to the VPS and runs ./mk/run 8011 single 1 remotely. After deploy, nginx should proxy HTTPS to 127.0.0.1:8011, and TURN should be reachable directly on 3478/udp (and 3478/tcp when enabled).
8. Test before and after deploy
cd frontend
npm run test:unit
cd ../backend
cargo test
Both commands certify the Svelte UX and Axum server behave after configuration changes.