--- type: reference title: "*arr Stack" description: "The *arr automation stack on FrankBook-Pro, every container on the VPN netns: topology, wiring, and gotchas." tags: [arr-stack, prowlarr, sonarr, radarr, lidarr, readarr, qbittorrent, frankbook-pro, dankserver] timestamp: 2026-09-04T00:00:00Z --- # *arr Stack The *arr automation stack runs on **FrankBook-Pro** (the Mac mini) as quadlets inside its podman machine; all egress rides the gluetun WireGuard netns. The per-service pages ([[Prowlarr]], [[Sonarr]], [[Radarr]], [[Lidarr]], [[Readarr]], [[qBittorrent]]) carry deployment facts; this page holds the cross-cutting API and auth-posture procedures. ## API version split (memorize before any *arr HTTP call) - Sonarr/Radarr: `/api/v3/…`; Lidarr/Readarr/Prowlarr: `/api/v1/…`. - Prowlarr is port **9696** — 8090 is Jackett. - **Empty body = endpoint absent**, not a failure: probe endpoints by status code first; v1 has no `/api/v1/profile` endpoint at all. - `PUT /downloadclient` and `PUT /indexer/{id}` take a SINGLE resource object (GET it, patch `settings`, PUT the whole object back). An array body fails with `Path: $ | BytePositionInLine: 1`. - API keys live in each container's config: `podman exec grep -o "[^<]*" /config/config.xml | cut -d">" -f2`. They are local-only secrets — they stop being secret the moment a vhost serves the API unauthenticated. ## Schema-first probing (validators are free documentation) Send the FULL schema template object with fields filled; v1 validators reject partials, and each error names the next required field — peel validators one live call at a time instead of guessing payload shapes: - `'Fields' must not be empty` → send a full schema copy, not `fields:[]`. - `'Base Url' must not be empty` when keys were camelCase → **PascalCase is required** (`BaseUrl`, `ApiKey`) — schema keys, not the JSON wire keys. - `DefaultMetadataProfileId must be > 0` → fetch real ids from `GET /api/v1/metadataprofile` (rows like `(1,'Standard')`) `GET /api/v1/qualitydefinition` returns the quality CATALOG — ids ≠ profiles. - `targetType null` → configContract not registered in that build (this Prowlarr build has no Prowlarr-plugin indexer; integration is Torznab-family only). - `testingPackageId` in a PUT body downgrades validation failures to warnings. - `syncLevel` is enum `ApplicationSyncLevel` — omit it or use the int; a string is rejected. **Verification rules:** `POST /downloadclient` returns the COLLECTION, not the created object — verify via GET. Test endpoints return verdicts in the BODY at HTTP 200 (`{}` = pass, a list = fail) — judge from bodies, not status codes. Command POSTs (`MoviesSearch`, `ApplicationSync`) run server-side and respond only when the work finishes: curl `-m` timeouts (HTTP 000) during a search are NOT failures — poll queue/history afterward instead of re-firing; warm small commands (`RefreshMonitoredDownloads`) answer in ~20 ms, so a hang means live work in progress, not a dead API. **DB as arbiter:** when endpoint shapes are opaque, stop the container and edit `/opt/prowlarr/config/prowlarr.db` with `sudo python3` sqlite directly, then start. The indexers table columns are Id/Name/Implementation/Settings/ConfigContract/Enable/Priority/Added — the enable flag column is `Enable` (not `EnableRss`). Prowlarr's real application settings values live in `prowlarr.db` → `Applications.Settings` JSON (the `/api/v1/applications` GET responses carry `fields: []`). ## Auth posture probe (ground truth is behavior, not config flags) `AuthenticationMethod=None` + `AuthenticationRequired=Enabled` in `config.xml` is ambiguous on paper; on the wire it means a login screen with **zero users** — first-account-wins. 1. **Confirm which host runs the service before probing** (`podman ps` on the target, not the box you ssh'd into first — the arrs live on FrankBook-Pro). 2. Probe an API endpoint that bypasses the SPA shell, locally and through any vhost: `curl -s -o /dev/null -w "%{http_code}" http://:/api/v3/system/status`. **SPA roots (`/`) always serve a 200 shell — never conclude "open" or "gated" from `/`.** 401 = gated; 200 + JSON = API fully open (anyone on the net can drive it with curl). 3. For config truth, dump the whole file (`podman exec cat /config/config.xml`), not a piped `grep | sed -n 2p`: **`sed -n Np` exits 0 on zero matches**, so the pipeline hides a miss and you conclude "empty field" when the grep itself failed. 4. Decision framing: when posture = gated-with-zero-users, present it as a decision menu (None behind the front-door wall vs External/authentikarr) not a silent change — first-account-wins means whoever logs in first owns the instance. ## The `None` + `Enabled` auth-config trap (v1 .NET apps) `None` + `Enabled` is self-contradictory: the UI Save is permanently disabled, and any save throws `DryIoc.ScopedItemException` in `AuthorizationMiddleware` → 500 on every page after restart. The valid no-auth representation is **absence of the `` key**, not the literal string `Disabled`. Fix = delete the key from `/config/config.xml` (`sed -i "//d"`), restart the container. Tell-tale: one v1 app boots clean while siblings 500-sprawl → diff their config.xml auth blocks; the healthy one shows the correct shape. (Prowlarr runs `Forms` + `Enabled` healthily — the crash combo is `None`+`Enabled` only.) General rules that saved the session these came from: container `State=running` ≠ app serving — read `podman logs --tail 12 ` before theorizing; when a UI gate blocks saving, go under it (these apps keep plaintext XML config on disk — read/edit via `podman exec`). ## See Also - [[Prowlarr]] — indexer feeds and the v2 sync model - [[Sonarr]] · [[Radarr]] · [[Lidarr]] · [[Readarr]] — the managers - [[qBittorrent]] — download client, 4.6.7 pin, E2E verification - [[FrankBook-Pro]] — the host and its podman machine - [[Homepage Status Probes]] — how the e2e gate treats this sleeping host