Same movie. Same second.
Real-time watch parties with synchronized playback, voice chat, reactions and private local-file playback.
Live app · Architecture · Design · Deployment
Flickfolk lets people open a room, share an invite and watch the same media on the same timeline. The server keeps room and playback state authoritative while each browser handles media playback locally.
- Room codes and invite links with no account required.
- Synchronized play, pause, seek and source changes.
- Host, Moderator and Participant roles with server-side permissions.
- Participant control requests with Host/Moderator approval.
- Late-join alignment, reconnect recovery and automatic host failover.
- Peer-to-peer WebRTC voice chat with speaking indicators and movie ducking.
- Reactions, text/GIF chat and responsive desktop/mobile layouts.
- Local-file watch parties without uploading the video.
- Direct media URLs, YouTube, HLS, MPEG-DASH and provider embeds.
| Source | Support |
|---|---|
| YouTube | Synchronized playback through the IFrame API |
| Direct video URLs | Native browser playback, including signed and extensionless media URLs |
HLS .m3u8 |
Native HLS where available, otherwise hls.js |
MPEG-DASH .mpd |
dash.js over the same synchronized video engine |
| Local files | MP4, WebM, MKV, MOV, AVI, MPEG, TS and other browser-decodable containers |
| Direct audio | MP3, OGG, M4A and other browser-supported audio |
| Twitch | VOD timeline sync plus capability-aware live playback |
| SoundCloud | Track and playlist playback through the official widget |
| Spotify | Official embed controls with provider-specific limits |
| Kick | Official livestream embed |
Container support still depends on codecs available in the viewer's browser and operating system. Flickfolk does not transcode media on the server.
Local files stay on each viewer's device. A Host or Moderator chooses a file, Flickfolk fingerprints a small sample in a Web Worker, and other viewers select their own copy.
Local File
↓
Browser Object URL
↓
HTML5 Video
↓
Flickfolk Sync Engine
Only a sanitized filename, size, duration and SHA-256 fingerprint are shared with the room. File bytes and filesystem paths never leave the device.
Exact copies match automatically. A different encode with nearly the same duration can be accepted explicitly with Use anyway. Refreshing intentionally requires selecting the local file again.
| Capability | Host | Moderator | Participant |
|---|---|---|---|
| Play / pause | Direct | Direct | Request |
| Seek | Direct | Direct | Request |
| Change source | Direct | Direct | Request |
| Approve requests | Yes | Yes | No |
| Promote / demote | Yes | No | No |
| Remove participant | Yes | No | No |
| Chat / reactions / voice | Yes | Yes | Yes |
Permissions are validated by the server; disabled client controls are only UI feedback.
Client intent
↓
Socket.IO
↓
Server validation + permissions
↓
Authoritative room/playback state
↓
Room broadcast
↓
Local playback + drift correction
The server owns room membership, roles, playback state and revisions. Browsers own media playback, UI state and peer-to-peer audio. The synchronization engine estimates clock offset, aligns late joiners, ignores stale revisions and corrects drift with small rate adjustments or seeks depending on the media provider.
apps/client React/Vite UI, player adapters, voice and room experience
apps/server Express, Socket.IO, validation, room state and signaling
packages/shared shared types, permissions, media parsing and sync policy
See ARCHITECTURE.md for the deeper implementation notes.
| Layer | Technology |
|---|---|
| Frontend | React 19, TypeScript, Vite |
| Backend | Node.js, Express, Socket.IO |
| Realtime audio | WebRTC |
| Playback | HTML5 Media, YouTube IFrame API, hls.js, dash.js |
| Styling | CSS, Figtree, JetBrains Mono |
| Testing | Vitest, Playwright |
| Delivery | Docker, GitHub Actions, Render |
Requires Node.js 22.12+ and npm.
npm ci
cp .env.example .env
npm run devProduction build:
npm run build
NODE_ENV=production npm startDocker:
docker compose up --buildThe production container runs as a non-root user and exposes /healthz.
WebRTC voice works without an account. Public STUN is available by default; restrictive networks can use TURN.
STUN_URLS=
TURN_URLS=
TURN_USERNAME=
TURN_PASSWORD=GIF search is optional and uses a server-side GIPHY key:
GIPHY_API_KEY=| Key | Action |
|---|---|
| Space / K | Play or pause |
| J / L | Seek -10s / +10s |
| F | Toggle stage fullscreen |
| Escape | Exit fullscreen / close transient UI |
| M | Mute microphone after joining voice |
/ |
Focus chat |
| C | Copy invite |
| T | Toggle sidebar |
Playback shortcuts still follow the current user's room permissions.
npm run lint
npm run typecheck
npm test
npm run build
npm run test:e2eCurrent suite:
- 220 unit/integration tests
- 41 Playwright browser tests
- Docker build and non-root health smoke test in GitHub Actions
Live: https://flickfolk.onrender.com
The React client and Socket.IO server run from the same Node service. Rooms are currently stored in memory, so active rooms are cleared when the service restarts.
Same movie. Same second.



