Architecture Overview
Lumastra runs one or more SvelteKit web nodes and one dedicated background worker, backed by PostgreSQL + pgvector and two separate Valkey instances. The web nodes serve SSR pages and API routes; the lean Node/TypeScript worker runs existing TypeScript tasks and plugins, with a supervised Rust file watcher inside the same worker container. An optional stream proxy handles Live TV independently.
Lumastra aggregates media metadata from multiple sources, runs AI-powered recommendations, and serves a reactive UI built from server-driven page sections. Here’s how requests, background work and shared state fit together.
The first diagram shows the components and request flow, including the separate task and cache Valkey instances. The full data tier, including PostgreSQL, is shown below.
Scroll, swipe, or click and drag to explore. Use the arrows to move, or fit the diagram above.
The layers
Section titled “The layers”- Browser / client: the SvelteKit UI (Svelte 5 runes), local search, an installable PWA service worker, and an HLS media player. Pages aren’t hardcoded: the server returns arrays of page sections that the client renders generically, so different users can see different layouts with no code changes.
- Web node(s): API routes and SSR pages, authentication (forward auth
headers for browsers; JWT/PAT tokens for
/api/v1), an image proxy, and a services layer (catalog, media and playback). Web nodes submit background jobs and provide task administration. A request flowshooks.server.ts → auth middleware → route handler → service layer → query module → database; routes issuing SQL directly is a lint warning, not a hard rule, so a handful of routes (e.g. the migration-status health check) still do. - Dedicated worker: one lean Node/TypeScript host runs BullMQ consumers, orchestration, scheduling and existing task/plugin code. Its supervised Rust watcher reports filesystem changes immediately. The watcher is a child process in the worker container; Docker and Kubernetes use the same arrangement. Web nodes run no task consumers, scheduler or file watcher. Combined web/worker mode is unsupported.
- Data & infrastructure: PostgreSQL 18 + pgvector stores catalog, users,
sessions, task history, embeddings and watch state. A persistent task Valkey
provides BullMQ storage and shared coordination under
noeviction. A separate cache Valkey holds disposable cached data and pub/sub underallkeys-lru. - Stream proxy: an optional separate service owns Live TV streams and media processes independently of worker restarts. See Live TV architecture.
Web nodes and the worker use PostgreSQL and both Valkeys. BullMQ runs against the task Valkey, while cache entries and pub/sub use the cache Valkey:
Scroll, swipe, or click and drag to explore. Use the arrows to move, or fit the diagram above.
Subsystems at a glance
Section titled “Subsystems at a glance”- Database follows a three-tier pattern: pure SQL in query modules,
transactional logic in services and service managers. Key tables include
media_items(movies, series, seasons, episodes, music),media_files(file paths linked to items),users,watch_history,media_embeddings(pgvector vectors),user_profiles(catalog and request embedding columns), andtask_history. See Architecture decisions for why the three-tier split exists instead of an ORM. Similarity queries always use the<=>(cosine distance) operator;<->(L2) and<#>(inner product) are never used, so every kNN query stays HNSW-eligible. - Task system: web nodes submit work through BullMQ; one dedicated worker handles metadata sync, image and frame extraction, and embedding generation. Existing TypeScript tasks/plugins keep their execution contracts. The Rust watcher reports library changes to that worker for ingestion and dispatch. Health and attention surfaces remain in the admin UI. See Task system for execution and recovery details.
- Cluster: multiple replicas coordinate via Valkey heartbeats (TTL keys for
live state) with transitions recorded in Postgres. Give each replica a stable
INSTANCE_ID. - AI: per-item embeddings (OpenAI or Voyage, bring-your-own-key) stored in pgvector drive similarity-based recommendations and the RAG chat assistant.
Valkey’s roles
Section titled “Valkey’s roles”The supported deployment uses two instances with different loss policies:
| Instance | Endpoint | Responsibility | Policy |
|---|---|---|---|
| Task Valkey | TASK_CACHE_URL | BullMQ jobs, live task state, locks and shared auth/session/playback coordination | Persistent storage, noeviction |
| Cache Valkey | CACHE_URL | Disposable cached metadata/search results and pub/sub notifications, including task cancellation and admin updates | Memory limit, allkeys-lru |
PostgreSQL remains the source of truth for durable task history and domain data. BullMQ uses the task Valkey for job delivery. Per-run logs and heartbeats also use Valkey; these responsibilities span more than a disposable metadata cache.
Configure distinct servers explicitly. Different database numbers or passwords
on one Valkey do not separate eviction policy. TASK_CACHE_URL is required and
never falls back to CACHE_URL; startup rejects equivalent normalized host/port
endpoints. Different DNS aliases or proxies still need operator verification
that they lead to separate instances. See
Deployment and Configuration.
External integrations
Section titled “External integrations”Lumastra serves media from local library paths; every integration below is optional and augments the catalog rather than sitting in the playback path.
| Integration | Role | Config |
|---|---|---|
| Jellyfin | Background sync source for movies, TV, and music (never in the playback path) | JELLYFINURL, JELLYFINAPIKEY |
| TMDB | Metadata enrichment: posters, descriptions, cast | TMDB_API_KEY |
| MDbList | Curated lists and rankings | MDBLIST_API_KEY |
| OpenAI | Text embeddings (text-embedding-3-*) for recommendations | OPENAI_API_KEY |
| Voyage AI | Text embeddings (voyage-3*), alternative provider | VOYAGE_API_KEY |
| Sonarr | TV series management | SONARRURL, SONARRAPIKEY |
| Seerr | Media requests | SEERRURL, SEERRAPIKEY |
Auth & identity
Section titled “Auth & identity”Auth methods and forward-auth headers are canonical to
Authentication. This page only needs the one-line
summary: browsers sign in via forward auth, built-in username/password, or OIDC;
third-party clients authenticate to /api/v1 via the OAuth 2.0 Device Authorization
flow, a personal access token, or a service token. See
Authentication for the full setup (including the
dev-only devUser/DEFAULT_USER overrides) and
API & tokens for the token surface.
How access is decided
Section titled “How access is decided”Every request passes hooks.server.ts before any route code runs. There,
resolveAuth works out who is calling: a session cookie, forward-auth headers
from a trusted proxy, or a token. Access gates then decide by the route
SvelteKit will dispatch to (event.route.id), never by the raw URL path, so
percent-encoded paths, a language prefix and __data.json requests are judged
like the plain page. A layout load is never an access gate, because data
requests and form actions skip it.
The route policy table (src/lib/server/auth/route-policy/rules.ts) gives every
route one policy: public, user, admin, admin-scoped (an admin, or a token
whose scope the route checks) or a token surface whose route authenticates
itself (/j, /subsonic, signed URLs, webhooks). A unit test fails for a route
without a rule, and a snapshot lists every route with its policy, so every change
to who may reach what shows up in review. The table currently runs in shadow
mode next to the existing gates and logs where they disagree; checks on a single
object (this title, this playlist) stay in the route handler.
Configuration
Section titled “Configuration”Configuration merges three layers at runtime, in ascending priority: compiled
defaults, the mounted /config/settings.json file, and environment variables.
Not every setting has an environment-variable equivalent: some admin UI
settings (like trickplay generation) live only in settings.json. See
Configuration for the full reference.