Lets Watch
Lets Watch is a local-first watch-party app for self-hosted environments. Each viewer opens the video file from their own machine, while the server only coordinates playback state over Socket.IO.
Features
Syncs play, pause,...
Syncs play, pause, and seek events across a room with shared participant control.
Lets viewers create...
Lets viewers create rooms with their own room code and 6-digit join PIN.
Offers room chat...
Offers room chat with viewer-selected names, quick emoji reactions, emoji-rendered text, and lightweight image sharing.
Serves the React...
Serves the React frontend and the Socket.IO backend from one Node process in production.
Keeps media local...
Keeps media local to each participant instead of uploading it to the server.
Broadcasts chat messages...
Broadcasts chat messages live without storing chat history on the server.
Includes basic hardening...
Includes basic hardening such as Helmet, payload validation with Zod, and lightweight rate limiting.
Obfuscates sensitive identifiers...
Obfuscates sensitive identifiers in backend logs so room and participant metadata are not written in plain text.
Applies dedicated authentication...
Applies dedicated authentication throttling to room create/join flows to reduce brute-force attempts, including reconnect-based retries from the same client identity.
Repository Layout
client/: React 19 + Vite frontend.server/: Express + Socket.IO backend.README.md: project overview and development guide.SECURITY.md: current controls, limitations, and security workflow.DEPLOYMENT.md: production setup and hosting notes.
Prerequisites
- Node.js 20 or newer.
- npm 10 or newer.
Local Development
Install dependencies in both packages:
npm run install:all
Start the backend:
cd server
npm run dev
Start the frontend in a second terminal:
cd client
npm run dev
The default local URLs are:
- Frontend:
http://localhost:5173 - Backend:
http://localhost:4000
Environment Variables
Server
Copy server/.env.example to server/.env:
PORT=4000
NODE_ENV=production
APP_URL=https://www.example.com
Client
Copy client/.env.example to client/.env if the frontend needs to talk to a separately hosted backend:
VITE_SOCKET_URL=http://localhost:4000
In production, the client falls back to window.location.origin when VITE_SOCKET_URL is not set.
Validation
Run from the repo root after both packages are installed:
npm run lint
npm run test
npm run build
What these commands do:
npm run lint: runs frontend ESLint and backend TypeScript checks.npm run test: runs client and server Vitest suites.npm run build: builds the frontend and type-checks the backend.
Production Notes
- The frontend must be built before starting the server in production.
- The backend serves
client/distwhenNODE_ENV=production. - Static assets under
/assetsare cached aggressively because Vite fingerprints them. index.htmlis served withno-storecaching so browsers always pick up the latest asset references.- In production,
index.htmlis served through a nonce-injecting response path. - Open the app at
/in production. Direct/index.htmlrequests are redirected to/so CSP nonce injection is consistently applied. - Chat images are resized in the browser before they are sent over Socket.IO.
- Joining an existing room requires both the chosen room code and its 6-digit PIN.
- Room codes are normalized to uppercase and PIN inputs are reduced to six digits during create and join.
- Rooms also expose a copyable share link with a room-specific token so invite links can join directly without re-entering the PIN.
- PIN-based joins rotate the current share token, so older copied links stop working after a fresh PIN join.
- Room state is in-memory only. When the last participant disconnects, room membership and playback state are dropped.
- Production Socket.IO admission checks
Originfirst and only falls back to the effectiveHostheader whenOriginis absent. It does not trustX-Forwarded-Hostas an auth signal.
Security Headers
- CSP is emitted as an HTTP response header and uses nonce-augmented
script-srcwith strict-dynamic support (withoutunsafe-inlinefor scripts). - HSTS is enabled in production with long max-age, subdomain coverage, and preload.
See DEPLOYMENT.md for a full deployment walkthrough.
License
This project is licensed under GPL-3.0-or-later. See LICENSE for details.