masqueradarr
architecture · 10 under the hood

Two packages, one image

A reference for how masqueradarr is built: the two-package layout, the container topology, the load-bearing boot order, the REST + WebSocket surface, the two Docker images, and what's still on the roadmap.

01 Two packages, not a workspace

Built and versioned independently

masqueradarr is two separate npm packagesnot a workspace, and they never import across the boundary. The Docker image stitches them together.

● / (root)

Vue 3 + Vite SPA

The management front end (hls.js, vue-router, mitt). TypeScript strict: false; imports carry no .js extension. Built with vue-tsc -b && vite build/dist.

● server/

Express 4 + Mongoose 8 API

ESM (type: module), TypeScript strict: true; every relative import carries an explicit .js extension even from .ts source. Serves the built SPA, the /api/* REST surface, and the WebSockets.

development

Two dev servers side by side: the SPA on :5173 (Vite, proxies /api:3000) and the API on :3000 (tsx watch, needs a reachable MongoDB). There is no test runner and no linter — correctness is verified by npm run build (type-check) in each package and by running the app.

02 Primary architecture

Clients, the container, the upstreams

The SPA and IPTV clients talk to the Express control plane, which owns MongoDB and drives the Rust data-plane sidecar for stream bytes.

the container topology control plane · data plane
clients
Browser
Vue 3 SPA
IPTV clients
TiviMate · VLC · Plex · Jellyfin
/api/* · WS · .m3u · bytes
masqueradarr container
one image · two processes
Express API — Node
control plane
gate + relay ↓
masq-proxy — Rust
data-plane sidecar
internal seams ↑ loopback
MongoDB
all the state
resolve · fetch · rewrite · pipe
upstream
Upstream IPTV sources
dulo · DaddyLive · Pluto · …
  • Sources adapter framework — a source-agnostic core (sync → normalize → dedupe → resolve) with ~17 adapters plus proxy-only direct / hdhomerun / local sources. See Sources.
  • Channel model — a pristine sourcechannels reference projected into an editable playlistchannels; edits survive re-syncs. Channels can be grouped into failover groups — one parent, ordered backups. See Playlists.
  • EPG + scheduler — multiple ingesters behind one sync path, driven by a croner scheduler over cronjobs. See EPG.
  • Video proxy engine — the remux-free Rust data plane. See Video proxy engine.
03 Boot sequence

The mount order is load-bearing

server/src/index.ts wires the app in a specific order. Mongo is the one fatal dependency; every subsystem after it must not crash boot.

index.ts · boot & mount order
1
config → mongo
Load config, connect MongoDB
fatal on failure A single no-retry connect; nothing runs without the DB.
2
startLogStore · bootInitSources · startScheduler · telemetry / statsHub
Start the subsystems
all non-fatal — must not crash boot Logging sink, index/migration reconcile, the scheduler, and the telemetry + stats hubs.
3
app.use('/api', authenticate)
Global auth — never blocks
Populates req.user from a session or stream token; it identifies, it doesn't gate.
4
/api/auth · /api/users · admin gates
Mount auth, then guard
requireAdmin on the adminOnlyRoutes prefixes + PUT /api/settings.
5
/api/* resource routers · sourcesRouter
Mount the REST surface
Each router is a thin Express layer over one Mongoose model; sourcesRouter mounts at root (paths span /api/sources).
6
SPA static + catch-all · error middleware
Serve the app, catch errors
Catch-all /^\/(?!api\/).*/ serves the SPA for non-/api routes; a 4-arg middleware is the only place a 500 ({ error: 'internal_error' }) is produced.
7
listen · server.on('upgrade')
Listen, then dispatch WebSockets
Upgrades are routed by pathname to the four WebSockets (below).
04 REST + 4 WebSockets

The request surface

Every routes/*.ts is a thin router over one model. Handlers are async (req,res,next) wrapped in try/catch(next); reads use .lean() + { _id: 0 }; error bodies are { error: '<snake_case>' }. Live data is pushed over WebSockets, not polled.

WebSocketCarries
/api/dulo/login-streamThe server-streamed Chromium dulo login capture
/api/stream-statsViewer / bandwidth / buffering telemetry (Active Streams + History / Metrics)
/api/logs-streamThe live "View logs" drawer feed
/api/system-statsLive CPU / memory push (Dashboard)
05 Two Docker images

One release publishes two products

Both are 3-stage builds (SPA → server → runtime) on Debian bookworm (glibc) under tini. They share the base and the apt Chromium; the one intentional divergence is the config bootstrap / supervisor.

● app.Dockerfile

The standard image

App only, for the compose stack — pairs with an external mongo service. The container self-provisions its config.json from .env on boot (docker/app-entrypoint.sh). No host config mount.

● aio.Dockerfile

The all-in-one image

App + mongod 7.0 + config bootstrap in one container. A root supervisor (aio-entrypoint.sh) writes config, starts mongod (--auth, loopback-only), waits for it, then starts node. One /data volume persists everything.

Multi-arch build & push (amd64 + arm64) is the /docker-build-all <tag> skill — it publishes both images in one release. The runtime layout (/app/dist, /app/public, /app/compose, /app/config, /app/seed-data) is load-bearing and stays in sync with server/src/paths.ts.

06 MASQ_EDGE topology

An opt-in inversion of the public socket

By default Node owns the public port and Rust is a loopback sidecar. MASQ_EDGE=1 inverts it — Rust binds the public port, serves streams in-process (token-gated via POST /api/internal/authorize, 30-second revocation TTL), and reverse-proxies the SPA, /api/*, downloads and all four WebSockets back to Node on a loopback port. Same public port and DOMAIN; reversible by clearing the flag. The topology diagrams and the "when to turn it on" guidance are on the Video proxy engine page.

07 Roadmap

What's live, and what's next

The rename and re-architecture are effectively complete — codebase, brand, runtime and images are all masqueradarr, and playback is live over the Rust engine (HLS + raw-TS). What's still pending is remux-shaped:

● live today

Working now

  • Resolve-on-demand catalog + ~17 adapters
  • Rust HLS + raw-TS proxy (retry, mirror rotation, buffer)
  • Failover groups — ordered channel backups, cross-provider
  • Users, roles, tokenized M3U / XMLTV export
  • Live telemetry, logs, scheduler, backup / restore
● on the roadmap

Designed / pending

  • Remux / transcode (HDHomeRun TS→HLS) — no ffmpeg yet
  • Seamless mid-stream failover splice (today: establish-time)
  • HDHomeRun-tuner & Xtream-panel emulation
  • ?fmt=ts per-request override; segmentCacheTtlSec
README · Primary Architecture · Public edge · Development CLAUDE.md · boot sequence · Docker contract restapi / styles-backend skills back to → Home
masqueradarr · architecture two packages · one image