docs(self-hosted): server storage mode, sign-in and the release note

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-10-12 00:16:56 +02:00
1 parent ced58e3ac0
commit 8714b3b445
7 files changed
+104 -29

No files matched your search

@@ -1,7 +0,0 @@
---
type: feature
area: self-hosted
issues: [305]
---
The Docker image now keeps a server database on a `/data` volume, the groundwork for sharing playlists and watch progress across devices. Mount a volume to keep it; nothing changes in the browser yet.
+8
View File
@@ -0,0 +1,8 @@
---
type: feature
area: self-hosted
issues: [305]
highlight: Playlists on your server
---
The Docker version now keeps playlists, favorites and watch progress on the server, so every phone, TV and computer that opens it shares one library. Playlists already in a browser move over with one click, and an optional access token keeps the library private.
+4
View File
@@ -135,6 +135,10 @@ The application is available at <http://localhost:4333>. See
compose file and [`docker/README.md`](./docker/README.md) for environment
variables, reverse proxy notes, PWA limitations, and build details.
The image keeps playlists, favorites and watch progress on the server, so
every browser that opens it shares one library. The first browser that opens
an updated instance offers to move the playlists it already has.
The self-hosted image runs the browser PWA rather than the Electron desktop app:
EPG/XMLTV panels, Embedded MPV, managed MPV/VLC launching, the download manager,
and Electron remote-control features are not available there. If browser
+20 -12
View File
@@ -18,9 +18,12 @@ The Docker image runs the browser PWA, not the Electron desktop application.
That means:
- EPG/XMLTV panels and multi-EPG views are not available in the self-hosted PWA.
- Playlist metadata is stored in the browser through IndexedDB, and Xtream user
data is stored by the PWA data source in localStorage. The PWA does not use
the Electron SQLite database or DB worker.
- With server storage (the image default) playlists, M3U favorites and
history, and Xtream favorites, recently viewed items and playback positions
live on the server, and every browser that opens the instance shares them.
Settings, parental locks, the global favorites order and UI preferences stay
per browser. With `IPTVNATOR_STORAGE=none` each browser keeps its own data
in IndexedDB and localStorage.
- Docker cannot launch local desktop players or Embedded MPV. If a stream does
not play inline in the browser, use the in-app copy URL action and open the
stream manually in an external player such as MPV, VLC, or IINA.
@@ -100,7 +103,7 @@ safe local defaults shown below.
| `NODE_EXTRA_CA_CERTS` | unset | Optional Node.js CA bundle path for providers using private certificate authorities. Mount the CA file into the container and set this path. |
| `IPTVNATOR_STORAGE` | `server` in the image | `server` opens the server storage database under `IPTVNATOR_DATA_DIR` and its `/api/v1` routes; `none` keeps the backend stateless. |
| `IPTVNATOR_DATA_DIR` | `/data` in the image | Data root; the database is `<root>/databases/iptvnator.db`. Mount a volume here. |
| `IPTVNATOR_ACCESS_TOKEN` | unset | When set, every route except `/`, `/health`, `/config.js` and `/v1/instance` requires `Authorization: Bearer <token>`, proxy routes included. The browser app cannot send a token yet: leave it unset until it can. |
| `IPTVNATOR_ACCESS_TOKEN` | unset | When set, every route except `/`, `/health`, `/config.js` and `/v1/instance` requires `Authorization: Bearer <token>`, proxy routes included. Each browser asks for the token once and keeps it. |
| `IPTVNATOR_MAX_DOCUMENT_MB` | `64` | Largest playlist document the backend accepts. Keep the nginx `client_max_body_size` in step. |
| `IPTVNATOR_BACKEND_START_TIMEOUT` | `300` | Seconds the entrypoint waits for the backend's `/health` before giving up. Database migrations run before the backend listens. |
@@ -136,16 +139,21 @@ the `restart: unless-stopped` policy.
The image opens a server storage database on the `/data` volume
(`IPTVNATOR_STORAGE=server`, `IPTVNATOR_DATA_DIR=/data`) and serves its API
under `/api/v1`. It is the backend half of keeping playlists, favorites,
history and playback positions on the server for every device; the PWA
starts using it in a follow-up change. Until then the volume holds an empty
database and each browser keeps its own data.
under `/api/v1`. The PWA finds it on start and keeps playlists, favorites,
history and playback positions there, so every phone, TV and computer that
opens the instance sees the same library. Settings → Server shows the
connection, the database size and path, and exports a backup.
Provider credentials will be stored unencrypted in that database, as the
The first browser that opens an instance with an empty database offers to
move the playlists it already kept, with their Xtream favorites and
progress; nothing is deleted from the browser. Later browsers can merge
theirs from Settings → Server.
Provider credentials are stored unencrypted in that database, as the
desktop app does. Do not expose an instance with server storage to the
internet without authentication in your reverse proxy. The backend already
supports `IPTVNATOR_ACCESS_TOKEN`, but the browser app cannot send the
token yet, so setting it now makes the app unusable. Back up the volume like any database: stop the container, or copy
internet without authentication. Set `IPTVNATOR_ACCESS_TOKEN` to require a
token: each browser then asks for it once on a sign-in page. Back up the
volume like any database: stop the container, or copy
`databases/iptvnator.db` together with its `-wal` file.
## IPv6, VPNs, And Connection Fallback
+2
View File
@@ -16,6 +16,8 @@ services:
# Server storage database on the /data volume. Set to "none" to keep
# the backend stateless.
IPTVNATOR_STORAGE: server
# Ask every browser for this token once before it shows the library.
# IPTVNATOR_ACCESS_TOKEN: change-me
volumes:
- iptvnator-data:/data
healthcheck:
+63 -8
View File
@@ -73,8 +73,8 @@ The current self-hosted PWA uses these `apps/web-backend` routes:
- `GET /xtream?targetId=<id>&username=<u>&password=<p>&action=<action>`
- `GET /stalker?targetId=<id>&macAddress=<mac>&action=<action>`
Server storage adds versioned routes under `/v1`; the PWA does not call them
yet:
Server storage adds versioned routes under `/v1`, which the PWA uses in
[server storage mode](#server-storage-mode):
- `GET /v1/instance` (always public): `{ service, apiVersion, instanceId,
storage: 'server' | 'none', auth: 'none' | 'token' }`
@@ -102,8 +102,8 @@ its database. Storage errors are logged by error name and code only, because
Drizzle messages can carry bound values such as portal credentials. With
`IPTVNATOR_ACCESS_TOKEN`, every route except `/`, `/health`, `/config.js`
and `/v1/instance` requires `Authorization: Bearer <token>`, the proxy
routes included. The PWA cannot send that token yet, so the variable must
stay unset until the browser side lands. Browser app state written through
routes included; the PWA sends it once its sign-in page has stored it.
Browser app state written through
`/v1/app-state` is stored under a `client:` key prefix, apart from
server-owned keys such as the instance id.
@@ -119,7 +119,11 @@ reached only `pnpm`/`nx`, and the orphaned server kept port 3333 after the run.
launches the backend through Nx or its `env` drifts from the `serve` target.
`WEB_BACKEND_PORT` (default 3333) relocates the E2E backend and the
`self-hosted.e2e.ts` URLs, like `MOCK_PORT` does for the mocks. Use it when
another worktree holds 3333. The `web:serve` entry can stay on Nx:
another worktree holds 3333. `server-storage.e2e.ts` talks to a second
backend from the same config, launched with `IPTVNATOR_STORAGE=server` on
`STORAGE_BACKEND_PORT` (default 3334) and a data directory named after the
Playwright process, so every run starts with an empty store; the spec
accepts at most that one extra launch. The `web:serve` entry can stay on Nx:
`@angular/build:dev-server` runs inside the Nx process, so the group kill
stops it. Playwright launches that command itself even outside CI: the
web-e2e targets carry no Nx `serve` dependency, because Nx refuses a
@@ -160,6 +164,55 @@ MPV and VLC preload launch methods (`openInMpv` and `openInVlc`), and
(`setMpvPlayerPath` and `setVlcPlayerPath`); a partial Electron bridge must not
expose desktop-only actions in the PWA/shared UI.
## Server Storage Mode
`main.ts` probes `GET /v1/instance` before bootstrapping, and only when the
deployment wrote a runtime `BACKEND_URL` into `window.__IPTVNATOR_CONFIG__`
(the Docker entrypoint does; hosted builds and Electron never probe). On
`storage: 'server'` it imports `apps/web/src/app/server-storage/` lazily and
merges `provideServerStorage()` into the app config. Everything else of the
client lives behind the `@iptvnator/services/server-storage` sub-entry, so
other builds never load it; `ServerStorageStatusService` and the token
helpers stay in the main `@iptvnator/services` barrel because the rail,
settings and guards read them in every build.
- **Playlists.** `ServerStorageSession.start()` (an app initializer) plugs
`ServerPlaylistDocumentStore` into `PlaylistStorageBackend` (kinds
`electron`, `server`, `indexeddb`), so `PlaylistsService` is unchanged. The
store caches full documents by ETag and uploads a document whose payload it
served unchanged with `keepPayload=1`, so a favorite or a rename does not
resend a large M3U.
- **Xtream user data.** `USER_DATA_STORAGE` becomes `ServerUserDataStorage`:
the four synced namespaces are cached under the `iptvnator-server:` key
prefix (the legacy unprefixed keys are never written in server mode) and
each change sends only the playlist entries that differ, debounced, with a
`keepalive` flush when the page hides. Start-up waits up to 3 s for the
first read.
- **Connection state.** `ServerStorageStatusService` is `online`, `offline`
(a request did not reach the server; retried every 30 s and when the tab
returns) or
`unauthorized` (a 401 clears the stored token). The rail shows a status
shortcut, an offline banner sits above the workspace content, and failed
writes raise a snackbar (`SERVER_STORAGE.WRITE_FAILED`, or `TOO_LARGE` for
a 413).
- **Access token.** With `IPTVNATOR_ACCESS_TOKEN` the instance reports
`auth: 'token'`; without a stored token, or after a 401, the guards route to
`/server-sign-in`, which checks the token against `/v1/instance/status`
before it stores it under `iptvnator-server:access-token` and reloads.
`ServerAccessTokenInterceptor` adds the header to `HttpClient` calls to the
backend, and `PwaService` adds it to its two raw `fetch` calls.
- **Moving browser data.** `ServerMigrationService` offers, once per server
instance (marker `iptvnator-server:migration:<instanceId>`), to move the
playlists this browser kept in IndexedDB. An empty server receives them
with their ids plus the legacy Xtream maps for those ids; a server that
already has playlists goes through the backup export and import, whose
fingerprints merge duplicates. Browser data is never deleted. Settings →
Server repeats the offer while the browser has playlists that were not
moved.
What stays per device: settings, parental lock PINs and locked groups,
the global favorites order and other UI preferences in browser storage.
## Runtime Limitations
The self-hosted build is the browser PWA, not the Electron desktop app. Keep
@@ -172,7 +225,9 @@ these limitations explicit in UI, troubleshooting, and release notes:
metadata uses `PlaylistsService` with IndexedDB, which
`PlaylistStorageBackend` selects when no SQLite bridge exists; Xtream
favorites, recently viewed items, playback positions, and cached collection
snapshots use `PwaXtreamDataSource` browser storage.
snapshots use `PwaXtreamDataSource` browser storage. In
[server storage mode](#server-storage-mode) both live on the server
instead.
- Browser playlist deletion must go through `PlaylistsService`, not
`DatabaseService`. `PlaylistsService.deletePlaylist()` runs registered
cleanup hooks such as the PWA Xtream cleanup so localStorage sidecar data does
@@ -271,8 +326,8 @@ and sidecar state in localStorage:
`PwaXtreamDataSource` and `PwaCollectionStorage` reach these keys through the
`USER_DATA_STORAGE` token from `@iptvnator/services` (default:
`localStorage`), never through `localStorage` directly, so a deployment can
substitute a server-synced storage.
`localStorage`), never through `localStorage` directly, so server storage
mode can substitute `ServerUserDataStorage`.
Entries should include a content snapshot when the item is added. Global
collection routes and the dashboard can then restore titles, posters, content
+7 -2
View File
@@ -89,7 +89,10 @@ The shell is intentionally split into four persistent regions:
recently viewed. The routed global-search rail link is Electron-only
because its data source is the SQLite worker bridge.
2. Provider-aware context links derived from the active or current playlist.
3. Settings remains a persistent footer shortcut in the rail.
3. Settings remains a persistent footer shortcut in the rail. In
[server storage mode](pwa-self-hosted.md#server-storage-mode) a server
status shortcut sits above it: its dot shows online, offline or
sign-in needed, and it opens Settings → Server.
4. No brand mark: it only repeated the first workspace link (Dashboard,
or Sources when the dashboard is off).
2. Top header:
@@ -103,7 +106,9 @@ The shell is intentionally split into four persistent regions:
6. Downloads shortcut in Electron.
3. Main body:
1. Optional left context panel.
2. Main router outlet content.
2. Main router outlet content. In server storage mode an offline banner
with Retry takes the first row of `.workspace-content-frame`, above
the scroller, so full-height pages keep their height.
4. Optional footer:
1. External playback session bar when a docked session is visible.