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 — library stats, highlights (most watched, most rewatched, longest movie, top device, location), genre breakdown and viewing time charts, up next, recently watched with platform/duration per play
- 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 - 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) |
SHOW_DEVICES |
No | Show device analytics (default: true) |
SHOW_LOCATIONS |
No | Show location analytics via geoip (default: false) |
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 <noreply@yourdomain.com>) |
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) |
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@yourdomain.com> (e.g. Plexo <noreply@yourdomain.com>).
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.
