# Architecture ## Goals - Watch the same title, in sync, with other users of the same Emby server. - Use Emby as the media source (catalog + streaming) without requiring changes to Emby itself or its official clients. - Access gated to people with an Emby account on this server — no anonymous use, no separate identity system. ## Non-goals - Chat/reactions — deliberately out of scope. - OIDC/SSO — an OIDC provider (LemonLDAP::NG) is available in principle, but the explicit choice was Emby-only auth: simpler, no extra infra dependency, and everyone already has an Emby account. - Full library navigation (Shows → Seasons → Episodes) in v1 — search-based catalog only, to keep the first version achievable. Revisit if browsing turns out to matter more than searching in practice. ## Why this replaced an Emby plugin The first attempt (still in [`src/WatchParty/`](../src/WatchParty/), kept for reference) was a real Emby Server plugin: sync via `ISessionManager`'s existing remote-control commands, a settings page via `IHasWebPages`, and — after a fair amount of reverse-engineering — a real user-facing page integrated into Emby's own web client menu (`PluginPageInfo.EnableInUserMenu`, undocumented but confirmed working; required a `data-controller` AMD module to satisfy Emby's modern `ViewManager`, found by reading the target server's actual `viewmanager.js`). That plugin builds and installs correctly. What it hit a hard wall on: **a button inside the video player itself**, or on an item's detail page. An Emby developer confirmed on their own forum that plugins have no API for this ("It's currently not possible for a plugin to do this"), and nothing found in research since suggests that's changed. The only way to get a button into Emby's own player would be patching Emby's shipped web client files directly inside the Docker container — outside the plugin entirely, guaranteed to break on every Emby image update, and requiring manual reapplication each time. That trade-off wasn't worth it. Building a standalone app sidesteps the question entirely: it's our own player, so there's no "how do I add a button to someone else's UI" problem in the first place, and sync can be tighter than nudging Emby's remote-control commands ever allowed (see below). Full trace of the plugin investigation (real errors, real fixes, sources consulted) is in the git history of this file and of `src/WatchParty/` — worth reading if a future Emby SDK update ever makes in-player integration possible and this gets revisited. ## Components ``` server/ Node.js + Express + ws. Owns party/room state and the realtime sync channel. Never touches a media byte - see "Media never flows through our backend" below. client/ Static frontend, plain JS + ES modules, no build step (same bias as the earlier plugin's control panel). Talks to two different things and must not confuse them: - Emby directly (embyApi.js): catalog, images, streaming. - Our own backend (party.js): create/join/leave a party, and the WebSocket for realtime sync. ``` ### Auth Emby-only, as decided. The browser calls Emby's own `POST /Users/AuthenticateByName` directly (same call the old plugin's control panel used) and keeps the resulting `AccessToken`/`User.Id` in `localStorage`. Our backend never sees a password — only ever a token the browser already obtained from Emby, sent as a header (`X-Emby-Token` / `X-Emby-UserId`) on REST calls, or as the first WebSocket message (not a query string — tokens don't belong in URLs). The backend validates a token by asking Emby itself (`GET /Users/{userId}` with that token) rather than trying to verify anything cryptographically — see `server/src/embyAuth.js`. ### Media never flows through our backend `client/js/embyApi.js` builds Emby URLs (images, HLS stream) directly, and the browser fetches them straight from Emby. `server/` only ever sees JSON party-coordination traffic and a handful of bytes per WebSocket message. The one deliberate exception: ``/`