# 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 and Emby access: the server proxies everything
Revised after the first version: the browser used to call Emby directly
(catalog, images, streaming) and only used `server/` for party coordination.
That hit real CORS problems in practice (Emby/its reverse proxy don't
answer cross-origin preflights the way that needed), and separately wasn't
the wanted shape anyway — the browser holding a real Emby access token at
all was more exposure than necessary. Current design:
- The browser sends a username/password **once**, to
`POST /api/auth/login` on **our own backend** — never to Emby directly.
- Our backend (`server/src/embyAuth.js`) calls Emby's
`POST /Users/AuthenticateByName` itself, using `EMBY_SERVER_URL` from its
own environment (not something the user ever types — see `.env.example`).
On success it mints its own opaque session id, stores
`{embyToken, userId, userName}` server-side (in-memory, like party state),
and hands the browser only that session id, as an `httpOnly` cookie
(`wp_session`). The browser never sees or holds Emby's real access token.
- Every other request — catalog search, item details, images, HLS
streaming, party actions — goes to **our own origin only**
(`/api/...`), authenticated by that same cookie. `server/src/embyProxy.js`
is what actually forwards these to Emby server-side and streams the
response back, attaching the session's real Emby token itself.
- The `/api/catalog/stream/:itemId/master.m3u8` and `.../raw` routes proxy
HLS specifically: the master playlist is fetched from Emby server-side,
and every URI line in it is rewritten to point back through `.../raw`
(whether Emby wrote it as relative or absolute) — otherwise hls.js would
follow the playlist's own URLs straight to Emby the moment it tried to
fetch a segment, defeating the whole point. See `rewritePlaylist` in
`embyProxy.js`.
- Images (``) and video (`hls.js`/`