Skip to content

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.

Components and request flow

Scroll, swipe, or click and drag to explore. Use the arrows to move, or fit the diagram above.

Components and request flow Browser / PWA Svelte 5 · HLS player ·service worker Auth in each web node forward-auth · JWT · PAT ·device flow Web node(s) SvelteKit · API / SSR · taskproducers Stream proxy (optional) Live TV · HLS · mediaprocesses One worker container Lean Node/TS · Rust watcherchild Task Valkey Persistent · noeviction ·BullMQ Cache Valkey Disposable cache · pub/sub ·allkeys-lru Library files Mounted inside the workercontainer External APIs TMDB · Jellyfin · OpenAI ·MDBList sign in HTTP · SSE HLS identity enqueue jobs deliver jobs immediate filesystem events cache · pub/sub cache · pub/sub metadata · images ingest · embeddings
  • 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 flows hooks.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 under allkeys-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:

Shared data stores

Scroll, swipe, or click and drag to explore. Use the arrows to move, or fit the diagram above.

Shared data stores Web node(s) HTTP / SSR · job producers One worker container Lean Node/TS · Rust watcherchild PostgreSQL 18 pgvector · source of truth ·embeddings Task Valkey TASK_CACHE_URL · persistent· noeviction Cache Valkey CACHE_URL · disposable ·allkeys-lru read / write enqueue · coordination cache · pub/sub task & media state consume · coordination cache · pub/sub
  • 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), and task_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.

The supported deployment uses two instances with different loss policies:

InstanceEndpointResponsibilityPolicy
Task ValkeyTASK_CACHE_URLBullMQ jobs, live task state, locks and shared auth/session/playback coordinationPersistent storage, noeviction
Cache ValkeyCACHE_URLDisposable cached metadata/search results and pub/sub notifications, including task cancellation and admin updatesMemory 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.

Lumastra serves media from local library paths; every integration below is optional and augments the catalog rather than sitting in the playback path.

IntegrationRoleConfig
JellyfinBackground sync source for movies, TV, and music (never in the playback path)JELLYFINURL, JELLYFINAPIKEY
TMDBMetadata enrichment: posters, descriptions, castTMDB_API_KEY
MDbListCurated lists and rankingsMDBLIST_API_KEY
OpenAIText embeddings (text-embedding-3-*) for recommendationsOPENAI_API_KEY
Voyage AIText embeddings (voyage-3*), alternative providerVOYAGE_API_KEY
SonarrTV series managementSONARRURL, SONARRAPIKEY
SeerrMedia requestsSEERRURL, SEERRAPIKEY

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.

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 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.