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
  • 13 trackers, 5 clients, 4 notifiers
  • 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-gated trackers — solved once, not once per request

RuTracker sits behind a Cloudflare challenge. A FlareSolverr instance solves it once; Marauder replays the resulting clearance on its own requests, so monitoring stays fast and still logs in.

  • The solver is a separate container you point Marauder at — the backend binary stays small and ships no browser
  • One solve per tracker, cached for the life of the clearance; a rejected clearance is re-minted automatically
  • Because the requests are Marauder's own, they can carry a login and a binary .torrent — a browser-proxied fetch can do neither
  • Tracker plugins opt in via the WithCloudflare capability — no global flag, no surprises
  • Prometheus counter for clearance mints, so you can see when something is forcing re-solves

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

Adding a topic used to mean finding the release in your browser first — fighting mirrors, Cloudflare, and login walls just to copy a URL. Now the add-topic form searches your trackers directly.

  • Type a title in the Search trackers tab; Marauder queries every searchable tracker concurrently and merges the results, sorted by seeders
  • Click a result and the topic URL is prefilled into the normal add flow — capability detection, poster preview, and quality options all apply unchanged
  • Rutor, Kinozal, LostFilm, and Anilibria search anonymously out of the box; RuTracker search uses your stored account and reports “needs a tracker account” instead of failing when you haven't added one
  • Cyrillic queries are transcoded to the cp1251 encoding RuTracker expects, so «Дюна» finds Дюна
  • Per-tracker fail-open: a slow or unreachable tracker shows a one-line notice while the others' results render normally

Configurable tracker domains + mirror fallback

Trackers move. When a primary domain is blocked or down, point the tracker at a working mirror — instance-wide, without recreating a single topic — and let Marauder rotate mirrors automatically when the current one starts failing.

  • Admin picks the active domain per tracker under Settings → Tracker domains — the plugin default or any known mirror (Kinozal .tv/.me/.guru, RuTracker .org/.net/.nl/.cr, LostFilm .tv/.win/.run, …), plus custom hostnames
  • Topics are identified by tracker + topic id, not hostname, so switching the domain reroutes every existing topic's checks and downloads immediately — nothing to recreate
  • Automatic fallback: repeated network-class failures rotate to the next mirror (gated by a failure threshold so a single blip doesn't switch everything, plus a 10-minute cooldown), and the admin is notified on rotation
  • A Test button probes a candidate and reports whether it serves a real page — a dead mirror answering an empty 200 shows “empty page”, not a false “reachable”
  • Custom domains are hostname-validated and admin-only; the probe vets resolved IPs at dial time and never follows redirects, so it can't be turned into an SSRF into your network

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