Unofficial implementation of the Nuvio API in a web UI (since there is none, yet).
Sign in with the Nuvio account you already use on mobile and you get the same profiles, addons, library and watch progress in a browser : plus a player that streams in the tab, casts to a TV, or hands the link off to a native app.
See every screen, light and dark
Nuvio Web hosts no media. All catalogs, metadata, streams and subtitles come from addons (the Stremio addon protocol) which you install and are responsible for. The app is a shell around whatever those addons return; it does not endorse, index, or verify any addon or its content. Use only addons you have the right to use in your jurisdiction. Not affiliated with or endorsed by Nuvio.
- The stable release
- The canary release
All deployed using Homerun
- Home : a rotating hero spotlight, continue watching, your library, and a row per catalog your addons expose.
- Discover : the full catalog browser: any catalog from any installed addon, filtered by genre, paginated.
- Search : fans out across every addon that serves a search catalog, and remembers your recent queries.
- Detail pages : synopsis, cast, IMDb rating, trailer, season and episode carousel, and a where to watch row (JustWatch) listing the official streaming, rent and buy options for your region.
- Command palette :
⌘K/Ctrl-Kfrom anywhere to jump to a screen or start a search.
- Source picker with quality, codec, size and release details parsed out of each addon's stream label, and an auto-pick that honours your preferred resolution.
- HLS and direct playback :
hls.jsfor.m3u8, the element's ownsrcfor everything else, with audio-track switching on either. - Subtitles from addons : SRT converted to WebVTT in the browser, so a subtitle file never touches the server : with size, colour, background plate and an auto-selected preferred language.
- Skip intro / outro via TheIntroDB. The keyless public tier works out of the box; add your own API key in Settings to raise the limits.
- Auto-play next episode, with an end-of-episode panel.
- Cast to a TV with no third-party SDK: the standard Remote Playback API (Chromecast on Chrome/Edge), falling back to WebKit's AirPlay hooks in Safari.
- Play in an external player : an Intent chooser on Android, VLC's
x-callback on iOS, the raw
magnet:for a P2P source, and copy-to-clipboard on desktop. - Playback diagnostics : the codec is probed before the stream is handed to
<video>, and playback that decodes no frames (unsupported HEVC/AV1) or no audio (Dolby Digital / DTS / Atmos) raises a dismissible banner instead of a black screen with no explanation. - Keyboard shortcuts : see below.
- Library, continue watching and history, kept per profile.
- Collections : your own folders of titles, separate from the library.
- Local-first sync store : an IndexedDB mirror with an optimistic write
queue and a background delta pull, so a bookmark or a progress save lands
instantly and reconciles later. Open tabs stay in step over
BroadcastChannel. - Watch stats and a full, editable history under Account.
- Profiles, with avatars, as on mobile.
- Themes : light / dark / system, a dim or AMOLED dark style, and seven accent colours.
- Settings for appearance, playback, sync, addons and integrations, stored on your Nuvio account so they follow you between devices.
- Accessible by default : every route is checked against WCAG 2 A/AA in CI (axe), including skip links and focus management.
Full docs live in docs/ and are published at
https://orochibraru.com/nuvio-web : install and configuration, every screen,
the self-hosting admin surface, the architecture, and how to work on it.
You need a Nuvio account (you can create one from the app's sign-up screen) and at least one addon. Addons installed on your account — mobile or web : show up everywhere.
The docker image is available
at Docker Hub, for
linux/amd64 and linux/arm64.
docker run -p 3000:3000 \
-v ./data:/app/data \
orochibraru/nuvio-web:latestservices:
nuvio:
image: orochibraru/nuvio-web:latest
restart: unless-stopped
ports:
- 3000:3000
volumes:
# Sessions, and your library / progress / history. Back it up.
- ./data:/app/data
environment:
# Behind a reverse proxy that terminates HTTPS : see Configuration below.
PROTOCOL_HEADER: x-forwarded-proto
HOST_HEADER: x-forwarded-host
healthcheck:
interval: 30s
retries: 3
start_period: 5s
test: ["CMD", "curl", "-fsS", "http://localhost:3000/api/health"]
timeout: 30sPut it behind a reverse proxy that serves HTTPS, open that address, sign in, pick a profile, and add an addon from Settings → Addons if your account has none yet.
Mount a volume on /app/data. It holds a small SQLite database: signed-in
sessions (Nuvio tokens, encrypted) and each profile's library, watch progress
and history. That database is the source of truth; it syncs both ways with your
Nuvio account in the background, so the Nuvio apps stay in step (see
Sync). Without the volume, a restart signs everyone out and
drops any change not yet pushed to Nuvio. Run one container per data directory.
Account, profiles, addons and settings still live on your Nuvio account. The
container serves the app on port 3000; GET /api/health answers 200 while it
is up, for HEALTHCHECK and orchestrator probes.
Serve it over HTTPS. SvelteKit rejects any write whose Origin header
doesn't match the app's own origin, and the image assumes https:// with the
host from the Host header. Behind a reverse proxy, name the headers that carry
the public scheme and host:
| Variable | Default | When you need it |
|---|---|---|
PROTOCOL_HEADER |
assumes https |
Behind a reverse proxy |
HOST_HEADER |
the Host header |
Behind a proxy that rewrites the host |
PORT |
3000 |
To listen on another port |
On plain HTTP (http://localhost:3000, a LAN address) the app renders, but
nothing saves: every write gets 403 Cross-site remote requests are forbidden.
SvelteKit 3 only takes a fixed origin at build time, so plain HTTP needs your
own image built with --build-arg NUVIO_BUILD_ORIGIN=http://…. The ORIGIN
variable from earlier releases does nothing now. Details in
Configuration.
Running it on the public internet is on you: put it behind HTTPS and whatever access control you would give any other self-hosted app.
In the player:
| Key | Action |
|---|---|
Space / K |
Play / pause |
← / J and → / L |
Seek 10s back / forward |
↑ / ↓ |
Volume |
M |
Mute |
F |
Fullscreen |
C |
Cycle subtitle track |
I |
Info overlay |
N |
Next episode |
E |
Episode list |
Esc |
Close the open panel |
Anywhere in the app, ⌘K / Ctrl-K opens the command palette.
- SvelteKit 3 / Svelte 5 (runes,
experimental.async), Tailwind 4 and shadcn-svelte components. - Streamed loads.
+page.server.tsreturns promises rather than awaiting them, so navigation completes on the page shell and each row fills in behind its own skeleton. Everything a page needs for its URL is fetched by the load — addon fan-out included : instead of costing an extra round trip after hydration. - Addon fan-out on the server, pooled rather than
Promise.all-ed (bounded concurrency, a per-request timeout, and a failed addon degrading to an empty row) so one slow addon can't hold a page hostage. Outbound addon fetches go through an SSRF guard that refuses private and link-local address ranges. - Remote functions are reserved for client-initiated work : a button, a right-click action, "load more".
- One binary in the image. The built app, Bun runtime embedded, is compiled
by
@sveltejs/adapter-buninto a self-contained server binary, so the runtime layer isdebian:slimplus that binary : no Bun, nonode_modules. - Tested at three levels : Vitest for the framework-agnostic logic (reconcile, stream parsing, codec probes, external-player URLs), Playwright for the flows, and axe for accessibility. Specs assert a clean console: an uncaught exception fails the run.
See contributing.md.
bun install
bun run dev # dev server on :5173
bun run check # svelte-check + tsc
bun run lint # biome + tailwind class lint
bun run test:unit # vitest
bun run test:e2e # playwright (needs Docker and a test account, see .env.example)Tracked in TODO.md
AGPL-3.0-or-later. If you run a modified version as a network service, you must offer its source to your users.