A modern, cinematic web client for Jellyfin — built to exercise the full Jellyfin API, read and write.
Open the live demo → — no install, no Jellyfin server of your own required.
JellyNext is an alternative front-end for your own Jellyfin media server. Where many clients stop at "browse and play," JellyNext aims for complete API coverage — if the Jellyfin API exposes it, JellyNext tries to surface it: metadata editing, image management, playlist sharing, admin dashboard controls, real-time messaging, remote control, watch-together, and more. Everything is bilingual (English / Türkçe) out of the box.
Note
This is an independent, community project. It is not affiliated with or endorsed by the Jellyfin project. You bring your own Jellyfin server.
- Screenshots
- Highlights
- Features
- Tech stack
- Architecture
- Getting started
- Configuration
- Available scripts
- Project structure
- Internationalization
- Security & privacy
- Roadmap & known limitations
- Contributing
- Acknowledgements
- License
| Home — billboard & title logos | Detail — metadata, versions, playback |
|---|---|
![]() |
![]() |
| Player — full controls | Admin — server dashboard |
![]() |
![]() |
- 🎨 Cinematic UI — full-bleed billboard with crossfade + auto-advancing progress indicator, title-logo artwork (Jellyfin
Logoimages and TMDb logos), a scroll-aware frosted-glass header, and a minimal, fade-first motion system that respectsprefers-reduced-motion. - 🔌 Full-API mindset — create/update/delete and actions, not just display. Metadata editing, image downloads, playlist sharing, admin server settings, scheduled-task triggers, API keys, backups, and more.
- 🔐 Token-safe by design — your Jellyfin access token lives in an httpOnly cookie and is injected server-side by a BFF proxy; it is never handed to page JavaScript for REST calls.
- ⚡ Real-time — a single WebSocket powers toast messages, a notification center, remote playback control, SyncPlay "watch together," and live admin session/activity updates.
- 🌍 Bilingual — every string is translated (EN / TR) via
next-intl, switchable at runtime. - 🧩 Discovery — an optional TMDb-powered "Discover" page surfaces worldwide trending/new titles (info-only), with a one-click check for what's already in your library.
Authentication & servers
- Server-first login — the first screen asks for your Jellyfin server address (no hardcoded server); optionally pre-filled from an env var.
- "Who's watching" multi-user picker and standard username/password sign-in.
- Quick Connect — approve a device that's signing in with a code, right from Preferences.
- Session is stored in an httpOnly cookie; the active server URL and UI language persist across visits.
Browse & discover
- Home — billboard hero, Continue Watching, Next Up, Your Favorites, server-computed Suggestions, "New in your library," Top Rated, per-library "Recently Added," and genre-discovery rows.
- Movies / TV libraries — rich filtering (genre, year, official rating, tags, studios, A–Z jump, quick watched/unwatched/favorites), bidirectional sorting (pick a field, toggle ascending/descending), and grid/list layouts. Sort + view choices sync server-side via DisplayPreferences so they follow you across devices.
- Discover (optional, TMDb) — worldwide trending & new movies/shows as info-only pages, with trailers and a "in your library?" match. The Discover tab only appears when a TMDb key is configured.
- Card quick actions — hover any poster to toggle watched or favorite without opening it (applied optimistically, so the card flips immediately and reconciles with the server afterwards), plus a
⋮menu for play/resume, reset progress (undo a resume point), add to a playlist, and jump to the parent series. - Search — debounced, with type chips (movies/shows/episodes/collections/people), a people row, and recent-search history.
- Music — albums & artists, album/artist detail, and a persistent Now-Playing bar.
- Live TV — channels, guide, and recordings (gracefully empty when Live TV isn't configured).
- My Stuff — Favorites, Collections, and Playlists.
Detail pages (movies · series · seasons · episodes · people · collections · playlists)
- Cast & crew, similar titles, special features, external links (IMDb/TMDb), taglines, ratings, and title-logo artwork.
- Write operations: favorite / mark-played toggles, add to collection or playlist, edit metadata, manage images (download from providers, reorder, set), Identify (re-match metadata), and delete.
- Playlist management — rename, public/private visibility, per-user sharing (view/edit), reorder, and delete.
- Person pages enriched with filmography and cross-navigation.
Player
- HLS full-timeline VOD playback (
hls.js) with accurate seeking and resume. - Audio & subtitle track selection (track-level), subtitle search / download / upload / delete, subtitle offset, secondary subtitles, and size preferences.
- Skip-intro, next-episode, play queue, chapters, playback-speed, Picture-in-Picture, volume boost/normalize, and a playback-stats overlay.
- All the classic keyboard shortcuts (space/k play-pause, ←/→ & j/l seek, ↑/↓ volume, 0–9 jump, f fullscreen, m mute, c captions,
</>speed, ⇧N/⇧P episode, and a?cheat-sheet). - Reports playback state back to the server; can be remote-controlled by other clients (including the official dashboard).
- SyncPlay — drive or follow a watch-together group.
Admin dashboard
- Server info, item counts, and live sessions (pushed over the WebSocket).
- Users — create, edit profile & policy, set/reset password, upload avatar, delete.
- Libraries — add/remove, edit paths, trigger scans.
- Scheduled tasks — run now and edit triggers (daily/weekly/interval/startup).
- Devices — rename or remove (your current device is protected).
- Plugins — browse the catalog, install, enable/disable, and edit configuration.
- API keys and backups management.
- Server settings — edit general config, metadata language/country, and login branding (disclaimer / custom CSS / splash screen).
- Send messages to and remote-control other sessions; restart / shut down the server.
Real-time & personalization
- Notification center in the header — merges admin "Send message" toasts with the server Activity Log, with client-side unread tracking.
- Per-user avatars — deterministic, name-derived colors (or your uploaded image), consistent everywhere.
- Preferences — max quality, autoplay next/trailers, subtitle defaults & size, audio normalize, reduce-motion, and language.
| Area | Choice |
|---|---|
| Framework | Next.js 16 (App Router, Turbopack, React Server Components) |
| UI | React 19, TypeScript 5, Tailwind CSS v4 |
| Data fetching / cache | TanStack Query v5 |
| Client state | zustand v5 (with persist for preferences) |
| Jellyfin types | @jellyfin/sdk (types + endpoint reference) |
| Playback | hls.js |
| i18n | next-intl v4 (EN / TR) |
| HTTP (server) | undici v8 (custom keep-alive dispatcher) |
| Icons | lucide-react |
| Tooling | ESLint 9, Prettier, Husky + lint-staged |
JellyNext is a Next.js App Router app split into two route groups:
(auth)— the login flow (server address → who's watching → sign in).(app)— everything else, gated on a server-side session check in the layout (redirects to/loginwhen unauthenticated).
All communication with Jellyfin flows through a Backend-for-Frontend (BFF) proxy, so the browser never holds the raw access token for REST:
flowchart LR
Browser["Browser (React / TanStack Query)"]
subgraph Next["Next.js server"]
JF["/api/jf/[...path]<br/>injects auth from httpOnly cookie"]
TMDB["/api/tmdb/[...path]<br/>injects TMDB_API_KEY (GET-only)"]
SOCK["/api/socket-info<br/>hands token to same-origin client"]
end
Jellyfin[("Jellyfin server")]
TheMovieDB[("TMDb API")]
Browser -- "cookie: jn_session" --> JF --> Jellyfin
Browser --> TMDB --> TheMovieDB
Browser --> SOCK
Browser -. "wss://.../socket?api_key" .-> Jellyfin
Key pieces
- BFF proxy (
app/api/jf/[...path]/route.ts) — forwards every Jellyfin request, injecting the access token + device id from the httpOnlyjn_sessioncookie. Passes throughRangeheaders and request aborts (signal) so seeking and track switches cancel cleanly. - Custom undici dispatcher (
lib/http/dispatcher.ts) — a boundedheadersTimeout(30s, against Node's ~5 minute default) is what stops a dead keep-alive socket from freezing every page that needs Jellyfin data when the server sits behind a reverse proxy / Cloudflare. Keep-alive itself is deliberately not ultra-short (30s): recycling sockets aggressively made nearly every request pay a fresh TCP + TLS handshake, which was felt most when starting playback. Outbound calls also carry a desktop browserUser-Agent, since Cloudflare bot protection can 403 a bare server runtime. - TMDb proxy (
app/api/tmdb/[...path]/route.ts) — GET-only; injectsTMDB_API_KEYserver-side and returns501when it isn't configured, keeping the key off the client. - Sessions (
lib/jellyfin/session.ts) — httpOnlyjn_session(server URL, token, user, device, admin flag); a readablejn_serverfor the active server; andjn_localefor the UI language. - Real-time (
components/shell/realtime-messages.tsx+app/api/socket-info/route.ts) — one WebSocket carries DisplayMessages, remote playback control, SyncPlay, live sessions, and Activity Log updates. - State — TanStack Query is the server-cache of record; zustand holds client state (preferences, play queue, remote-control command bus, SyncPlay, notifications).
jellynext-demo.vercel.app — a live instance pointed at the Jellyfin project's public demo server. Pick the demo user and leave the password blank.
Rather run it yourself? Same thing, one command:
docker run --rm -p 3000:3000 \
-e JELLYFIN_SERVER_URL=https://demo.jellyfin.org/stable \
ghcr.io/mugurc/jellynext:latestNote
The demo server is read-only and shared, so write actions (favorites, metadata edits, admin screens) will fail there. Point JellyNext at your own server to use them.
docker run -d --name jellynext -p 3000:3000 \
-e JELLYFIN_SERVER_URL=https://jellyfin.example.com \
--restart unless-stopped \
ghcr.io/mugurc/jellynext:latestOr with Compose — a ready compose.yaml is in the repo:
docker compose up -dImages are published for linux/amd64 and linux/arm64. Every variable is optional: with none set, JellyNext just asks for your server address at login. Because they're read at request time, the same image works against any server — no rebuild needed to change it.
Prerequisites
- Node.js 20.9+ (22 LTS recommended)
- npm (or pnpm / yarn / bun — examples use npm)
- A running Jellyfin server you can reach from where you run JellyNext
# 1. Clone
git clone https://github.com/mugurc/jellynext.git
cd jellynext
# 2. Install dependencies
npm install
# 3. (Optional) configure environment — see "Configuration" below
cp .env.example .env.local # then edit
# 4. Start the dev server
npm run devOpen http://localhost:3000 and enter your Jellyfin server address on the welcome screen.
Production build
npm run build
npm run startAll configuration is optional — JellyNext works with zero env vars (you'll just type your server address at login, and the Discover page will be disabled). Set them in the container environment, or in a .env.local (git-ignored) when running from source:
| Variable | Scope | Required | Description |
|---|---|---|---|
JELLYFIN_SERVER_URL |
server only | No | Pre-fills the server address on the login screen; users can still enter a different one. If unset, login begins with server-address entry. Read at request time, so it can be changed on a prebuilt image without rebuilding. |
TMDB_API_KEY |
server only | No | A TMDb v3 API key. Enables the Discover page (worldwide trending/new + title logos). Without it, the Discover tab and its route are hidden entirely — the rest of the app works normally. |
NEXT_PUBLIC_JELLYFIN_SERVER_URL |
client | No | Deprecated alias for JELLYFIN_SERVER_URL, still honoured. Being NEXT_PUBLIC_, it is inlined during next build and cannot be changed afterwards — prefer the server-only name. |
Example .env.local:
# Optional: pre-fill the login server field
JELLYFIN_SERVER_URL=https://jellyfin.example.com
# Optional: enable the Discover page (kept server-side, never sent to the browser)
TMDB_API_KEY=your_tmdb_v3_api_keyA committed .env.example lists the same variables with empty values, so cp .env.example .env.local gives you a filled-in starting point.
| Script | What it does |
|---|---|
npm run dev |
Start the dev server (Turbopack) |
npm run build |
Production build |
npm run start |
Serve the production build |
npm run lint |
Run ESLint |
npm run lint:fix |
ESLint with autofix |
npm run typecheck |
tsc --noEmit |
npm run format |
Prettier write |
npm run format:check |
Prettier check |
A Husky pre-commit hook runs lint-staged (ESLint + Prettier on staged files).
src/
├─ app/
│ ├─ (auth)/login/ # server-address → who's-watching → sign-in
│ ├─ (app)/ # session-gated app shell
│ │ ├─ home, movies, tv, music, live, discover, search, my-stuff, prefs
│ │ ├─ item/[id] # unified detail (movie/series/episode/album/…)
│ │ ├─ watch/[id] # player
│ │ ├─ person/[id]
│ │ └─ admin/… # dashboard, users, libraries, plugins
│ ├─ api/
│ │ ├─ jf/[...path] # BFF proxy → Jellyfin (auth injection)
│ │ ├─ tmdb/[...path] # BFF proxy → TMDb (key injection, GET-only)
│ │ └─ socket-info # hands socket credentials to the client
│ ├─ globals.css # theme tokens + motion system
│ └─ layout.tsx
├─ components/ # feature UIs (home, detail, player, admin, media, shell, …)
├─ lib/
│ ├─ jellyfin/ # queries, mutations, session, playback, media helpers
│ ├─ http/dispatcher.ts # undici keep-alive agent
│ ├─ player/ · notifications/ · prefs/ · search/ # zustand stores & helpers
│ ├─ tmdb/ # TMDb queries
│ └─ auth/current-user.tsx # session context
├─ i18n/ # next-intl config + request handler
├─ messages/ # en.json · tr.json
└─ config/nav.ts
- Powered by next-intl; locales live in
src/messages/{en,tr}.json. - The selected language persists in a
jn_localecookie and can be toggled from the header. - Contribution rule: every user-facing string must exist in both
en.jsonandtr.json. Adding a new language is as simple as adding amessages/<locale>.jsonand registering it insrc/i18n/config.ts.
- Access token stays in an httpOnly cookie; REST requests are proxied so page JavaScript never reads it.
- TMDb API key is server-only and injected by the proxy.
- WebSocket caveat: a browser WebSocket can't send httpOnly cookies, so the token is surfaced to the authenticated, same-origin client to open the socket. This does not widen the attack surface — same-origin JS can already act as the user via the proxy — and it mirrors how the official Jellyfin Web client works.
- JellyNext talks only to the Jellyfin server you configure and (optionally) TMDb. There is no telemetry.
Some areas are implemented against the API but can't be fully exercised without the right server data/hardware, and may be rough:
- Live TV beyond channels/recordings (guide grid, timers, series timers, tuner hosts, listing providers).
- Trickplay scrubbing thumbnails.
- Lyrics and deeper music features.
- DLNA profiles.
Jellyfin's legacy per-user Notifications API is gone on modern servers, so the notification center is built on the Activity Log instead.
Contributions that flesh these out (ideally with a server that has the data) are very welcome.
Contributions are welcome! A few guidelines that keep the project consistent:
- Completeness over minimalism. When you touch a feature, ask "does the API expose more here?" and build the create/update/delete/action, not just the display.
- Bilingual. Add EN and TR strings for every new piece of text.
- Green checks. Keep
npm run build,npm run typecheck, andnpm run lintpassing. - Match the surrounding code — mirror existing patterns (query hooks in
lib/jellyfin, portaled modals, the motion utilities, etc.).
# before opening a PR
npm run typecheck && npm run lint && npm run build- Jellyfin — the free software media system this client is built for.
- The Movie Database (TMDb) — powers the optional Discover page. This product uses the TMDb API but is not endorsed or certified by TMDb.
- Next.js, TanStack Query, Tailwind CSS, zustand, hls.js, and lucide.
Released under the MIT License © 2026 Mahmut Uğur Çeker. See LICENSE for details.



