Skip to content
! Early days. Marauder v1.0 is young: the core is validated end-to-end, but many tracker plugins are still alpha — structurally complete and unit-tested, not yet validated against live services. Expect rough edges. See plugin status →

deep dive

What's inside Marauder

A tour of every system that makes Marauder feel boring to operate: the auth model, the plugin architecture, the scheduler, the observability stack, the frontend, the deployment story, and the CI pipeline.

Authentication you can trust

Security is opt-out, not opt-in. Every secret on disk is encrypted, every JWT is signed with an asymmetric key, every login is audited.

  • Local accounts with Argon2id password hashing (time=3, memory=64 MiB, parallelism=4)
  • ES256 JWT access tokens, opaque refresh tokens stored as SHA-256 hashes server-side
  • Refresh-token rotation with reuse-detection — a replayed token revokes the entire token family
  • AES-256-GCM encryption at rest for tracker credentials, client configs, and JWT signing keys
  • Master key loaded from MARAUDER_MASTER_KEY env var, 32 bytes base64-encoded, never persisted in plain
  • OpenID Connect sign-in via coreos/go-oidc — pre-built Keycloak realm in the SSO compose profile
  • Async audit log for every login success / failure / logout, visible to admins in the UI
  • Multi-user with strict per-user data isolation at the database WHERE-clause level

Plugin architecture

A new tracker is one Go file implementing one interface. The init() function self-registers with a global registry; main.go just blank-imports the package.

  • Tracker interface: CanParse, Parse, Check, Download — plus optional WithCredentials, WithQuality, WithCloudflare capabilities
  • Client interface: Test, Add, ConfigSchema — supports magnet URIs, raw .torrent bytes, and watch-folder writes
  • Notifier interface: Test, Send — Telegram, Email, Webhook, Pushover bundled
  • 16 trackers, 5 clients, 4 notifiers shipped in v1.0
  • Per-plugin E2E test harness with a fake qBittorrent — every tracker plugin runs through Parse → Login → Verify → Check → Download → submit-to-qbit on every CI run
  • Forum-tracker plugins share a common SessionStore that holds a cookie jar per (tracker, user_id) pair so concurrent topic checks reuse the same logged-in client

Cloudflare bypass — without bundling chromium in the main binary

NNM-Club and other Cloudflare-protected trackers go through a separate sidecar service. The main backend binary stays small.

  • Standalone cfsolver Go service using chromedp + Debian-slim chromium
  • Exposes POST /solve {url} → { user_agent, cookies[] }
  • Built as a separate Docker image, started via the cfsolver compose profile only when needed
  • Tracker plugins opt in via the WithCloudflare capability — no global flag, no surprises
  • Trivial to swap for FlareSolverr later if chromedp falls behind

Scheduler with backpressure and observability

The scheduler is a single dispatch goroutine plus a bounded worker pool. Failures back off exponentially; a busy run never overflows the queue.

  • Configurable tick interval (default 60 s) and worker pool size (default 8)
  • Per-topic check pipeline: load → call tracker Check → compare hash → call Download if changed → decrypt client config → call client Add
  • Exponential backoff on errors, capped at 6 hours; success resets to the topic's configured interval
  • Falls back to the user's default client if a topic has no explicit client_id
  • Per-topic notifier override: a topic routes its update alerts to one chosen notifier (validated as owned by the user), or fans out to the global notifier set when unset
  • Per-topic replace-on-update: when a single-release topic is updated, optionally remove the previously delivered torrent from its client — and its files from disk — instead of stacking duplicates (qBittorrent, Transmission, Deluge, µTorrent). Default keeps every version; per-episode trackers are never touched
  • In-memory ring buffer of the last 50 run summaries with checked / updated / errors counters, exposed at /api/v1/system/status and visualised on the System page
  • Per-tracker check duration histograms in Prometheus

Sonarr integration — take over what Sonarr can't track

Sonarr grabs the initial release from a forum tracker but can't keep monitoring that updateable topic for future changes. Marauder reads Sonarr's grab history and automatically takes over.

  • Admin-configured under Integrations → Sonarr: Sonarr URL + API key (encrypted at rest, never returned), poll interval, allowed trackers, and default client / category / download dir
  • Background poller reads GET /api/v3/history/since (grabbed events) and matches each grab's topic URL (data.nzbInfoUrl) to an installed tracker plugin
  • Auto-creates a monitored topic per supported grab, owned by the admin and badged “Sonarr” in the Topics list
  • Go-forward on first enable (no historical flood), idempotent (deduped by topic URL — a season-pack grab makes exactly one topic), and fail-open if Sonarr is unreachable
  • Future torrent updates deliver to the same client + category Sonarr imports from, so Sonarr keeps importing completed files with no category drift

Observability from day one

Structured logs, metrics, health checks, audit log — all of it standard, all of it opt-out, all of it documented.

  • Prometheus /metrics endpoint, gated by a static bearer token (MARAUDER_METRICS_TOKEN)
  • Metrics: HTTP request count + duration histograms (with chi route patterns to keep cardinality bounded), scheduler runs, per-tracker checks + duration histogram, per-tracker updates, per-client submit results
  • Structured JSON logs via zerolog, every request gets a request_id, secrets are scrubbed
  • /health and /ready endpoints with the standard semantics
  • Live System status page in the frontend (auto-refreshes every 5 seconds)
  • Admin-only Audit log page

Frontend that doesn't look like an admin template

React 19 + Vite 8 + Tailwind 4 + shadcn/ui. Dark-first, glass cards, blue primary, amber accent, framer-motion entry animations.

  • Zustand auth + prefs stores, TanStack Query for server state
  • Animated login, dashboard with live status tiles, topics list with bulk-edit + density toggle
  • Clients CRUD page with per-plugin field hints and Test-connection buttons
  • Notifiers CRUD with Send-test buttons
  • System status page (any user) and Audit log page (admin)
  • English + Russian UI with a tiny zustand-backed i18n module — the locale switcher lives in the header
  • RFC 7807 problem-details error rendering throughout

Deployment that doesn't surprise you

Docker compose, three Docker images, three compose overlays, one master key. No hidden state, no host dependencies, no manual cert ops.

  • Multi-stage Dockerfiles for backend, frontend, and cfsolver — non-root users, healthchecks, alpine/debian-slim bases
  • deploy/docker-compose.yml: production stack (db + backend + frontend + nginx gateway)
  • deploy/docker-compose.dev.yml: overlay with port publishing + real qBittorrent + Transmission for E2E testing
  • deploy/docker-compose.sso.yml: overlay with Keycloak 26 + a pre-imported realm and a test user
  • All host-exposed ports live in the safe 34xxx range (gateway 34080) to avoid colliding with other services

CI / CD that takes itself seriously

Six GitHub Actions workflows, golangci-lint with 12 rules, Trivy scans, cosign signing, CycloneDX SBOMs, Dependabot.

  • ci.yml: backend race-tests + golangci-lint + govulncheck, frontend tsc + build, cfsolver build/vet — under 3 minutes per PR
  • docker.yml: builds all 3 images on push to main, Trivy scan with HIGH/CRITICAL fail
  • e2e.yml: nightly + on-tag full compose-stack walkthrough (magnet → qBittorrent end-to-end)
  • client-acceptance.yml: nightly matrix creating every supported client against a real container — pinned baseline + a latest-version canary that auto-files an issue when an upstream release breaks a client
  • release.yml: tag-pushed multi-arch (amd64 + arm64) build, cosign keyless signing via OIDC, CycloneDX SBOM per image, GitHub Release with auto-extracted CHANGELOG
  • codeql.yml: GitHub CodeQL SAST for Go and TypeScript with the security-extended query pack
  • dependabot.yml: weekly grouped updates for gomod, npm, github-actions, docker base images