No description
  • TypeScript 47%
  • Svelte 21.4%
  • Rust 12.8%
  • Shell 7.6%
  • CSS 7.6%
  • Other 3.5%
Find a file
2026-05-19 14:45:27 -07:00
backend security hardening 2026-05-19 14:45:27 -07:00
docs e2ee 2026-05-18 10:27:55 -07:00
frontend security hardening 2026-05-19 14:45:27 -07:00
infra security hardening 2026-05-19 14:45:27 -07:00
mk security hardening 2026-05-19 14:45:27 -07:00
scripts refactor for smaller code 2026-05-07 19:04:44 -07:00
.dockerignore checkpoint, still broken 2026-03-31 11:40:59 -07:00
.gitignore checkpoint 2026-04-28 09:38:34 -07:00
AGENTS.md iOS to linux chromium working 2026-03-18 23:56:29 -07:00
backlog-methodology.md backlog 2026-05-05 15:52:29 -07:00
current-hypotheses.md checkpoint 2026-04-28 09:38:44 -07:00
Dockerfile security hardening 2026-05-19 14:45:27 -07:00
failed-approaches.md checkpoint 2026-04-28 09:38:44 -07:00
README.md multi user mode checkpoint 2026-05-10 12:52:53 -07:00
todo.md update todos 2026-05-19 11:19:37 -07:00

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 4 total participants on desktop.
  • Mobile multi-party is best-effort and should stay at 3 total 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 adaptiveStream and dynacast remain enabled, and the existing local camera publish plan stays on the current h540/h720 simulcast ladder. As rooms grow, quality may drop to balanced, constrained, or audio-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

  1. One Ubuntu 24.04+ VPS (minimum 2 vCPU, 4 GiB RAM) with ports 22, 80, 443, 3478/udp, and 3478/tcp open in your firewall.
  2. One DNS A record pointing your chat domain (for example meet.example.com) to the VPS IP.
  3. Docker, Node 20+, Rust 1.83+ toolchain, and Certbot with the nginx plugin 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 with openssl 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. Default 3478.
  • CATCHUP_TURN_ENABLE_TCP: advertise and serve TURN over TCP on the same port. Default 1.
  • 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 from CATCHUP_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 60 second abuse-throttle window on the sensitive issuance endpoints.
  • Current first-release limits are:
    • /create-room: 8 requests per source IP per 60 seconds
    • /api/livekit/token: 30 requests per source IP per 60 seconds
    • /turn-cred: 30 requests per source IP per 60 seconds
  • 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-For IP 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:

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.