Core Structure
This page explains how JellyWatchParty is built at a system level. For implementation-level detail, see the Technical Reference section (Protocol, Server, Client, Plugin, Sync Algorithms, Host Bridge).
System Overview
JellyWatchParty consists of three main components that work together to provide synchronized media playback.
┌─────────────────────────────────────────────────────────────────────────┐
│ Jellyfin Server │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ JellyWatchParty Plugin (C#) │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌────────────────┐ │ │
│ │ │ ClientScript │ │ Configuration │ │ JWT Token │ │ │
│ │ │ Endpoint │ │ Page │ │ Endpoint │ │ │
│ │ └─────────────────┘ └─────────────────┘ └────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
│
│ HTTP (loads JS)
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Browser (Jellyfin Web) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Web Client (JavaScript) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ State │ │ UI │ │Playback │ │ WS │ │ Utils │ │ │
│ │ │ Module │ │ Module │ │ Module │ │ Module │ │ Module │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
│
│ WebSocket
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Session Server (Rust) │
│ ┌─────────────────┐ ┌─────────────────┐ ┌────────────────────┐ │
│ │ Room Manager │ │ Client Handler │ │ Message Router │ │
│ └─────────────────┘ └─────────────────┘ └────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
Components
1. Jellyfin Plugin (C#)
Integrates with Jellyfin’s plugin system.
Responsibilities:
- Serve the client JS loader (
/JellyWatchParty/ClientScript) and each individual module it fetches (/JellyWatchParty/Client/{path}) - Provide configuration UI for JWT settings
- Generate JWT tokens for authenticated users
- Bridge native (non-browser) Jellyfin sessions in as room hosts — see Host Bridge
For the normal browser flow, the plugin backend makes no outbound network calls to the session server — it only ever hands the browser a token and a URL, and the browser does the talking. The one exception is Host Bridge, which opens its own WebSocket to the session server on behalf of a bridged native session. Full detail: Plugin.
2. Session Server (Rust)
A lightweight WebSocket server that manages rooms and relays messages.
Responsibilities:
- Accept WebSocket connections and manage room lifecycle (create, join, leave, close)
- Relay playback events between clients, validating host permissions
- Filter state updates (anti-jitter, rate limiting) and schedule synchronized actions
All state is in-memory (Arc<RwLock<HashMap<...>>>) — no database, and
everything is lost on restart, since rooms are ephemeral by design. Full
detail: Server.
3. Web Client (JavaScript)
Modular JavaScript injected into Jellyfin’s web interface.
Responsibilities:
- Inject UI elements (button, panel, home section)
- Manage the WebSocket connection to the session server
- Intercept video playback events and apply synchronized playback commands
- Correct drift with playback rate adjustment, and synchronize clocks with the server
Full detail: Client.
Data Flow
Joining a Room
Browser Server Host Browser
│ │ │
├── WebSocket connect ──────────►│ │
│◄─── client_hello ──────────────┤ │
│◄─── room_list ─────────────────┤ │
│ │ │
├── join_room ──────────────────►│ │
│◄─── room_state ────────────────┤ │
│ ├── participants_update ──────►│
│ │ │
├── ready ──────────────────────►│ │
Synchronized Playback
Host Browser Server Client Browser
│ │ │
├── player_event (play) ──────►│ │
│ │ │
│ [Validate host] │
│ [Calculate target_ts] │
│ │ │
│◄─── player_event ────────────┼─── player_event ──────────►│
│ target_ts = T+1000 │ target_ts = T+1000 │
│ │ │
│ [Wait for T] │ [Wait for T] │
│ video.play() │ video.play() │
Host Disconnect (Grace Period, Then Closure)
A dropped connection (Wi-Fi blip, tab throttling, app backgrounding) doesn’t close the room right away — the server holds the host’s slot open for a 90-second grace period before tearing anything down:
Host Server Participants
│ │ │
X (disconnect) │ │
│ [90s grace period starts] │
│ │ │
│ (reconnects within 90s) │ │
├── WS connect ?client_id=... ──►│ │
│ [same client_id: reattach] │
│◄── room_state (resent) ────────┤ │
│ [nothing broadcast to participants — no visible disruption]│
────────────────────────────────────────────────────────────────
OR, if the host never reconnects and others remain:
│ [promote earliest-joined participant]│
│ ├── host_changed ────────────►│
│ │ (room stays open) │
────────────────────────────────────────────────────────────────
OR, if the host never reconnects and no one else is left:
│ ├── room_closed ────────────►│
│ ├── room_list ──────────────►│
See Server: Reconnect and Room Lifecycle for the full mechanics, including the persistent client ID that makes reattachment possible.
Technology Stack
| Component | Technology |
|---|---|
| Plugin | C# (.NET 9.0), ASP.NET Core |
| Session Server | Rust, Axum, Tokio |
| Web Client | JavaScript (IIFE pattern) |
| Communication | WebSocket, JSON |
| Authentication | JWT (optional) |
| Containerization | Docker, Docker Compose |
State Shape
Server (per client and per room, all in-memory):
Client { sender, room_id, user_id, user_name, authenticated, message_count, last_reset, last_seen }
Room { room_id, name, host_id, clients, ready_clients, pending_play, state,
last_state_ts, last_command_ts, chat_history (capped at 50), password_hash }
Client (JWP.state): ws, roomId, clientId, isHost, serverOffsetMs,
lastSyncPosition, lastSyncServerTs, isSyncing, isBuffering, and more —
see Client: state.js for the full list.
Security Model
- Authentication: Optional JWT tokens validated by session server
- Authorization: Only hosts can send playback commands
- Transport: WebSocket (ws://) or secure WebSocket (wss://)
- Rate limiting: 30 messages/sec per client, 10 token requests/min per user
- Message size: 64KB maximum
See Security for the full security model and threat analysis.
Operational Limits
| Resource | Limit | Configurable |
|---|---|---|
| Clients per room | 20 | Server constant MAX_CLIENTS_PER_ROOM |
| Hosted rooms per user | 1 | Not configurable — creating a new room closes any room the user already hosts |
| Messages per second | 30 | Server constant RATE_LIMIT_MESSAGES |
| Message size | 64 KB | Server constant |
| Token requests | 10/min per user | Plugin constant |
| Metric | Typical Value | Notes |
|---|---|---|
| Sync accuracy | ±50ms | Under normal network conditions |
| Clock sync precision | ±20ms | After EMA smoothing stabilizes |
| Drift correction range | 0.85x - 2.0x | Playback rate adjustment |
| State update interval | 1000ms | From host to server |
| Sync loop interval | 500ms | Client-side drift check |
For scaling limits and capacity planning, see Deployment: Capacity Planning. For the detailed sync algorithms behind drift correction and clock sync, see Sync Algorithms.