| Provider | Status |
|---|---|
| Plex | ✅ Validated — DASH direct-stream (HEVC passthrough), playback-identity, dashboard/Tautulli reporting |
| Jellyfin / Emby |
The features below describe the validated Plex experience. Jellyfin/Emby may work via the HLS path but have not been tested in the current build — treat them as unsupported for now.
- A guild admin runs
/setup configureonce to connect their Plex server. - A member runs
/play <title>. The bot creates a voice channel for the watch party (under a "Watch Parties" category) and posts an embed with the voice channel and a "Get my watch link" button. - Each viewer taps the button for their own watch link (so the party knows who's who), opens it in the browser, and joins the voice channel — playback is synchronized automatically over WebSocket.
- Pause / resume / skip / back-10s work from both Discord commands and the watch page, apply to everyone in that party, and show a toast of who did it.
- The participant list reflects who's in the voice channel and who's watching via their link (web-only viewers are marked).
Starting a party: /play → the bot posts the voice channel and a personal-link button.
Each viewer taps the button for their own session-scoped link — only they can see it.
Every control action is attributed — the party sees who paused, skipped, or resumed.
Each watch party is its own voice channel, so a single server can run several parties at once — different groups watching different things. Within one party, everyone shares a single upstream transcode (N viewers ≈ the load of one).
Video is streamed via DASH and proxied through nginx directly from Plex — it never passes through Discord or Node.js, and is never stored anywhere.
These steps are for the person running the bot. You only do this once.
- Docker and Docker Compose
- A way to expose nginx (the
NGINX_PORTbelow) publicly over HTTPS — a reverse proxy, tunnel, or whatever you already use. Setting that up is out of scope here; you just need the resultinghttps://URL forPUBLIC_URL. - A browser that can decode HEVC (Chrome 107+, Edge, or Safari) — same requirement as the Plex web app, since video is direct-streamed as HEVC
- Go to the Discord Developer Portal and create a new application.
- Under Bot, add a bot and copy the token — this is your
DISCORD_BOT_TOKEN. - Under General Information, copy the Application ID — this is your
DISCORD_CLIENT_ID. - No privileged intents are required. (The bot uses the Guild Voice States intent, which is not privileged — no portal toggle needed.)
Create a directory for the bot and pull down the two files the stack needs — the compose file and the example env file. (Both images, including the nginx config, are published; nothing is built locally.)
mkdir discordwatchparty && cd discordwatchparty
curl -O https://raw.githubusercontent.com/Bukowskaii/DiscordWatchParty/main/docker-compose.yml
curl -o .env https://raw.githubusercontent.com/Bukowskaii/DiscordWatchParty/main/.env.exampleGenerate an encryption key (used to encrypt stored credentials at rest) and paste it into .env:
openssl rand -hex 32
# prints 64 hex chars — set this as ENCRYPTION_KEY in .envThen open .env and fill in the rest:
DISCORD_BOT_TOKEN= # from step 1
DISCORD_CLIENT_ID= # from step 1
ENCRYPTION_KEY= # the `openssl rand -hex 32` output above
PUBLIC_URL= # the public https:// URL that reaches nginx, e.g. https://watchparty.example.com
NGINX_PORT=8780 # host port nginx listens on; point your public proxy here
# Optional:
# SESSION_TTL_MINUTES=360 # watch-link lifetime (default 6 hours)docker compose up -dThis pulls and starts two containers, both from published images: nginx (public-facing, port 8780, ghcr.io/bukowskaii/discordwatchparty-nginx) and the bot (internal, not exposed, ghcr.io/bukowskaii/discordwatchparty).
Slash commands register automatically. On startup and whenever it joins a new server, the bot registers its
/commands per-guild (instant, no propagation delay). You do not need to run a separate command-deploy step. (A manualnpm run deploy-commandsscript also exists, but only from a source checkout — it isn't needed for the image-based setup above.)
Point your reverse proxy / tunnel at http://<host>:8780 (or your NGINX_PORT) so it's reachable at the https:// URL you set as PUBLIC_URL. Setting that up is your call — anything that terminates TLS and forwards to nginx works. If you change PUBLIC_URL after starting, apply it with:
docker compose restartlocalhost inside Docker refers to the container, not your host. Use your machine's LAN IP instead (e.g. http://192.168.1.100:32400). Keep this internal — the bot streams Plex segments directly over the LAN, so routing it back out through the public tunnel would be slow and pointless.
Use this URL, replacing YOUR_CLIENT_ID with your DISCORD_CLIENT_ID:
https://discord.com/api/oauth2/authorize?client_id=YOUR_CLIENT_ID&permissions=19472&scope=bot+applications.commands
This grants View Channels, Send Messages, Embed Links, and Manage Channels. Manage Channels is required — the bot creates and deletes a voice channel per watch party. (If you invited an earlier version, re-invite with this URL or add Manage Channels to the bot's role, or /play will fail to create channels.)
Run this in your Discord server (requires Manage Server permission):
/setup configure provider:Plex url:http://192.168.1.x:32400 token:your-plex-token
Find your Plex token: support.plex.tv/articles/204059436
The bot tests the connection (and verifies library access) before saving. /setup status shows a live health check of the stored credentials.
By default, playback is attributed to your Plex account in the dashboard and Tautulli. To attribute it to a dedicated user instead:
- In Plex, create a Home managed user (Settings → Users & Sharing → Home) and share your libraries with them.
- Configure it by name — the bot looks up that user's server access token automatically:
/setup configure provider:Plex url:... token:... playback-user:Watch Party
Use playback-user:none to revert to your own account. /setup status shows which identity is in use and whether its token works.
Jellyfin/Emby (
api-key:) are accepted by/setupbut, as noted above, are untested in the current build.
| Command | Who | Description |
|---|---|---|
/setup configure |
Admins | Connect Plex (and optionally set a playback-user) |
/setup status |
Admins | Live health check of the configuration (credentials redacted) |
/setup remove |
Admins | Remove this server's media configuration |
/play <query> |
Everyone | Search (with autocomplete) and queue into your watch party — creates a voice channel if you don't have one |
/search <query> |
Everyone | Preview a result (with details) before adding it to the queue |
/queue |
Everyone | Show your party's queue and playback position |
/pause · /resume |
Everyone | Pause / resume your party for all its viewers |
/skip |
Everyone | Skip to the next item in your party's queue |
/stop |
Everyone | End your party and delete its voice channel |
Search & TV: /play and /search return movies and shows. Pick a show to drill into seasons → episodes via menus (paged when there are many). Commands act on the party you're currently in (by voice presence, or the one you started).
Cleanup: a party is torn down automatically when its voice channel is empty and no one has the watch page open for ~2 minutes, or immediately on /stop.
- Credentials never leave the host. Stored encrypted (AES-256-GCM) in
data/guild_configs.dbusing theENCRYPTION_KEYfrom your.env. The database and the key are useless without each other. - Watch links are session-scoped. Each link contains a random token that expires after
SESSION_TTL_MINUTES(default 6 hours) and is validated server-side on every request. Sessions are in-memory and reset when the bot restarts. - nginx is the only public entry point. The Node.js bot port is not exposed. The internal auth endpoint (
/_internal/auth) is markedinternaland cannot be reached externally. - Video bytes never pass through Node.js. nginx validates the session token, then proxies DASH segments directly from Plex.
Keep ENCRYPTION_KEY backed up separately from data/. If you lose the key, existing configurations cannot be decrypted and admins will need to re-run /setup configure.
Pull the latest published images and recreate the containers:
docker compose pull
docker compose up -d
# Slash commands re-register automatically on startup.If a release changes docker-compose.yml itself (e.g. new services or env vars), re-fetch it first with the same curl command as in Host setup, then docker compose up -d.
This project was built with the assistance of Claude (Anthropic). Architecture decisions, feature direction, and code review are driven by the project author. Claude serves as a development accelerator — handling implementation details, debugging, and boilerplate while human judgment guides what gets built and how.

