Personal media dashboard for your Plex library. Track your movies, TV shows, watch history, and viewing patterns — all in one self-hosted interface.
Built with Next.js 16, tRPC, Tailwind CSS v4, Recharts, and Framer Motion.
- Dashboard — live Now Watching sessions with playback progress and stream quality, library stats, highlights, genre and viewing-time charts, up next, and recent activity
- Movies — full library grid with genre/watched/unwatched filters and search, click any movie for details (summary, cast, ratings, watch history)
- TV Shows — library grid with episode progress bars and completion filters, per-season episode breakdown with missing episode detection
- Music — artist grid with genre filter and search, click any artist to browse albums and tracks (gated behind
SHOW_MUSIC=true) - Analytics — 8 chart visualizations with configurable time range (7d/30d/MTD/90d/year), period navigation with left/right arrows
- Global Search — press K to search movies and TV shows, filter by genre or director
- Smart Caching — tiered TTLs (library 1hr, metadata 30min, analytics 15min, activity 5min), server-side image cache (24hr)
- Recommendations — visitors can recommend movies/TV shows via a dialog: search TMDB (with "In Library" badges for titles you already have), enter name + optional message, submit. Notifications via Resend, SMTP, or Discord webhook. Optional Turnstile captcha with rate limit bypass. Optional Overseerr integration to auto-create requests.
- Wishlist — dashboard section showing pending items from Overseerr requests and Plex Watchlist, merged and deduplicated
- Admin Panel — Cmd+L to view cache status, purge caches, test Discord/email notifications (protected by secret + optional Turnstile)
- Privacy Controls —
SHOW_DEVICESandSHOW_LOCATIONSenv vars to control what data is exposed - Multi-user Activity — optional, privacy-aware viewer avatars/names, activity filtering, and period-aware Top Viewers analytics backed by Tautulli; identities are stripped from browser data by default
- Docker — multi-stage build, pushes to GHCR via GitHub Actions
- Analytics — optional Plausible integration with self-hosted instance support
Plexo pulls data from two sources on your home network:
-
Plex Media Server — library contents, metadata, watch status, on-deck items. The
PLEX_TOKENdetermines whose watch data is shown (each Plex user has their own token). All requests go through a server-side API client that adds the token, so it never reaches the browser. -
Tautulli — watch history, play statistics, device/platform info, IP geolocation. Tautulli monitors your Plex server and provides detailed analytics that Plex itself doesn't expose.
Data flow:
Browser → Next.js tRPC → In-memory cache → Plex/Tautulli APIs
↓
Cache hit? Return cached data
Cache miss? Fetch from API, cache with TTL, return
All API calls are server-side only ("server-only" imports). The browser never talks to Plex or Tautulli directly. Poster images are proxied through /api/plex-image to keep the Plex token off the client and cached server-side for 24 hours.
tRPC procedures return { data, lastUpdatedAt } so the UI always knows how fresh the data is. TanStack Query handles client-side caching with 5-minute stale time and 30-minute auto-refresh.
Your Plex token determines which user's watch data you see.
- Sign in to Plex Web (
app.plex.tv) or your server's web UI - Open any media item and click Get Info (or View XML)
- In the URL bar you'll see
X-Plex-Token=xxxxxxxxxxxx— that's your token - Alternatively: browser dev tools → Network tab → look for
X-Plex-Tokenin any request
For a managed/home user: Sign in as that user (switch from top-right menu), then grab their token from the network tab. Each user has their own token. Library content is the same — only watch progress differs.
- Open your Tautulli web UI → Settings → Web Interface
- Copy the API Key (or click Generate to create one)
To scope Tautulli stats to a single user:
- In Tautulli, go to Users → click the user
- The URL shows
user_id=12345678— set that asTAUTULLI_USER_ID
Leave empty to see stats for all users.
git clone https://github.com/davidilie/plexo.git
cd plexo
cp .env.example .envEdit .env — see .env.example for all available options.
pnpm install
pnpm devOpen http://localhost:3000.
docker build -t plexo .
docker run -p 3000:3000 --env-file .env plexoOr pull from GHCR:
docker pull ghcr.io/davidilie/plexo:latest| Variable | Required | Description |
|---|---|---|
PLEX_URL |
Yes | Plex server URL |
PLEX_TOKEN |
Yes | Plex authentication token (determines which user's data) |
TAUTULLI_URL |
Yes | Tautulli instance URL |
TAUTULLI_API_KEY |
Yes | Tautulli API key |
TAUTULLI_USER_ID |
No | Scope Tautulli stats to one user |
REFRESH_SECRET |
Yes | Secret for cache admin panel (Cmd+L) |
DISPLAY_NAME |
No | Name shown on dashboard (default: "David") |
APP_URL |
No | Base URL for metadata/OG images (default: localhost:3000) |
NEXT_PUBLIC_VERSION |
No | Footer version label (default: dev; Docker builds set this to the Git commit hash) |
SHOW_DEVICES |
No | Show device analytics (default: true) |
SHOW_LOCATIONS |
No | Show location analytics via geoip (default: false) |
VIEWER_DISPLAY |
No | Viewer identity in activity: hidden, avatar, name, or avatar-name (default: hidden) |
SHOW_MUSIC |
No | Show music library (artists, albums, tracks) (default: false) |
PLAUSIBLE_ENABLED |
No | Enable Plausible analytics (default: false) |
PLAUSIBLE_DOMAIN |
No | Domain for Plausible tracking |
PLAUSIBLE_SCRIPT_URL |
No | Self-hosted Plausible script URL |
PLAUSIBLE_API_URL |
No | Self-hosted Plausible event API URL |
RECOMMEND_ENABLED |
No | Enable recommend feature (default: false) |
TMDB_API_KEY |
No | TMDB API key for searching movies/TV (required if recommendations enabled) |
TURNSTILE_SITE_KEY |
No | Cloudflare Turnstile site key (enables captcha on form) |
TURNSTILE_SECRET_KEY |
No | Cloudflare Turnstile secret key (enables server-side verification + rate limit bypass) |
RESEND_API_KEY |
No | Resend API key for email notifications |
RESEND_FROM |
No | Resend sender address (e.g. Plexo <[email protected]>) |
RECOMMEND_EMAIL_TO |
No | Email address to receive recommendations (used by Resend and SMTP) |
SMTP_HOST |
No | SMTP server hostname |
SMTP_PORT |
No | SMTP server port (default: 587) |
SMTP_USER |
No | SMTP username |
SMTP_PASS |
No | SMTP password |
SMTP_FROM |
No | SMTP sender address |
DISCORD_WEBHOOK_URL |
No | Discord webhook URL for recommendation notifications |
OVERSEERR_URL |
No | Overseerr instance URL (enables request creation + dashboard wishlist) |
OVERSEERR_API_KEY |
No | Overseerr API key |
RECOMMEND_AUTO_REQUEST |
No | Auto-create Overseerr request on recommendation (default: false) |
IMAGE_OPTIMIZE |
No | Re-encode Plex images to WebP via sharp (default: true). Set false to serve the upstream Plex bytes unchanged. |
IMAGE_OPTIMIZE_QUALITY |
No | WebP quality 1-100 when IMAGE_OPTIMIZE=true (default: 85). Higher = sharper + larger. |
IMAGE_UPSTREAM_SCALE |
No | Multiplier for the resolution pulled from Plex before sharp downsamples (default: 2). 2× super-sampling yields sharper posters; set to 1 to save CPU/bandwidth. |
When TAUTULLI_USER_ID is empty, activity and watch analytics include all users known to Tautulli. VIEWER_DISPLAY controls how active viewers appear:
| Value | Activity rows | Viewer filter | Browser data |
|---|---|---|---|
hidden |
No identity | Hidden | Usernames and friendly names are removed |
avatar |
Profile image with name on hover/focus | Friendly names | Names are exposed only through the identity tooltip |
name |
Friendly name | Friendly names | Avatar URLs are not exposed |
avatar-name |
Profile image and friendly name | Profile images and friendly names | Both are exposed |
The dashboard labels a visible multi-user feed as Household Activity. The full Activity page adds a viewer filter, while Analytics lets you fan out and select the viewer avatars to scope viewing charts to one person. Analytics also ranks active viewers for the selected period, and media watch-history rows carry the same identity treatment. Avatars are fetched through Plexo’s same-origin proxy; Plex/Tautulli credentials and source avatar URLs never reach the browser.
Set TAUTULLI_USER_ID to keep the entire deployment scoped to one Tautulli user. This server-side scope always overrides viewer filters.
The recommend feature lets visitors suggest movies/TV shows to you. To enable it:
- Create an account at themoviedb.org
- Go to Settings → API → Request an API Key
- Copy your API key (v3 auth)
You need at least one channel configured to receive recommendations. All configured channels fire in parallel.
Discord (easiest): Create a webhook in your Discord server (Server Settings → Integrations → Webhooks) and set DISCORD_WEBHOOK_URL.
Resend: Sign up at resend.com, get an API key, and set RESEND_API_KEY, RESEND_FROM, and RECOMMEND_EMAIL_TO. The RESEND_FROM address must use a domain you've verified in Resend, formatted as Name <[email protected]> (e.g. Plexo <[email protected]>).
SMTP: Set SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_FROM, and RECOMMEND_EMAIL_TO.
RECOMMEND_ENABLED=true
TMDB_API_KEY=your_tmdb_api_key
# Plus at least one notification channel from aboveA "Recommend" button with a heart icon will appear in the navbar.
To add captcha protection:
- Go to Cloudflare Turnstile and create a widget
- Set
TURNSTILE_SITE_KEYandTURNSTILE_SECRET_KEY
When Turnstile is configured, the captcha widget appears on the recommendation form. If a user hits the 5/hr rate limit, they can verify via Turnstile to reset their limit and continue.
| Tier | TTL | What |
|---|---|---|
| Library | 1 hour | Sections, genres |
| Metadata | 30 min | Movie/show listings |
| Analytics | 15 min | Computed aggregations |
| Activity | 5 min | On-deck, history |
| Images | 24 hours | Plex poster images |
Cmd+L opens the admin panel to view cache entries and purge all. Also available via API:
curl -X POST https://plexo.yourdomain.com/api/refresh \
-H "Authorization: Bearer your-refresh-secret"| Tool | Purpose |
|---|---|
| Next.js 16 | App Router, RSC, standalone output |
| React 19 | UI |
| TypeScript 5.7+ | Strict mode |
| Tailwind CSS v4 | Styling (OKLCH color system) |
| shadcn/ui | Component library |
| tRPC 11 | Type-safe API layer |
| TanStack Query 5 | Data fetching + caching |
| Recharts 3 | Chart visualizations |
| Framer Motion | Animations |
| nuqs | URL state management |
| date-fns 4 | Date formatting |
| @takumi-rs/image-response | Dynamic OG images |
| next-plausible | Analytics (optional) |
If you find Plexo useful, consider supporting my work on Zerocut.
