<- Back to Projects

LetsWatch

Lightweight self-hosted watchlist and release-tracking web app focused on clean UX and low-overhead deployment.

TypeScriptSelf-hostedWeb AppAutomation

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/dist when NODE_ENV=production.
  • Static assets under /assets are cached aggressively because Vite fingerprints them.
  • index.html is served with no-store caching so browsers always pick up the latest asset references.
  • In production, index.html is served through a nonce-injecting response path.
  • Open the app at / in production. Direct /index.html requests 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 Origin first and only falls back to the effective Host header when Origin is absent. It does not trust X-Forwarded-Host as an auth signal.

Security Headers

  • CSP is emitted as an HTTP response header and uses nonce-augmented script-src with strict-dynamic support (without unsafe-inline for 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.

Background by Mymind