← The specification

Service auto-wiring

Rendered from spec at76d46f3 (2026-10-25) · its source · Edit this page

Status: Accepted · Audience: Operator · Area: D — Content & household


Purpose

Connect the services to each other, so the operator never copies an API key between two web interfaces.

A working stack requires roughly thirty connections: each *arr needs both download clients registered, each needs root folders, Prowlarr must push indexers to every *arr, Bazarr needs Sonarr and Radarr, Seerr needs Jellyfin plus Sonarr and Radarr, Homepage needs an API key from all of them. Every one is configured by hand, in a different UI, by copying a value from somewhere else.

That’s the half-hour of clicking that defines the current experience — and it must be redone from scratch after any configuration loss, which is why people are afraid to touch a working stack.

Behaviour

Keys are read, not asked for

Each service generates its own API key on first start and writes it to its configuration. lemonfiber reads them directly. The operator never sees or handles them — the copying is the problem being solved.

qBittorrent is the exception, and it runs the other way. It mints a temporary WebUI password on every start and asks for it to be replaced, so there is nothing durable to read. lemonfiber therefore generates the password, sets it, and records it where the VPN’s forwarded-port push can authenticate with it (D1-R16). Without that step the tunnel acquires a port on every connect and cannot apply it, which reports as healthy and costs the operator the peer connectivity port forwarding exists to buy.

NZBHydra2 is the other exception. It starts with no authentication at all, and in that state it answers a read of its whole configuration to anyone who can reach it, including the indexer accounts it holds and their keys. An indexer account is an account somebody paid for. Loopback binding keeps the network out, but not every other container on the stack’s network, a plugin’s among them. So lemonfiber turns authentication on: it names an administrator, generates the password, and records it under the service’s own administrator-password setting (NZBHYDRA2_ADMIN_PASSWORD), as it does for the media server. It then proves the change by asking the same read again presenting nothing, and being refused (D1-R22). The connections that reach NZBHydra2 with its API key are not affected: the key is a separate credential, and nothing that searches through it changes.

An NZBHydra2 already running with no authentication, which is every stack seeded before this, is adopted rather than rebuilt. Authentication is turned on in place, keeping every indexer, setting and history it holds, and the indexers it held before are read back afterwards (D1-R23). An operator who later turns it off, or changes it, has made a choice. Seed keeps that choice and reports it as drift, like any other value (D1-R3, D1-R24). The doctor still says what that choice exposes (D1-R26), and a reset turns authentication back on. A password lemonfiber no longer holds, or one NZBHydra2 refuses, is reported as refused, naming the setting. lemonfiber never turns authentication off, or replaces the service’s administrator, to get back in (D1-R25).

The wiring graph

From To What
lemonfiber qBittorrent WebUI password — generated, set, and recorded for the forwarded-port push
lemonfiber NZBHydra2 Authentication — an administrator set, its password generated and recorded
SABnzbd, qBittorrent Sonarr, Radarr, Lidarr, Bindery Download client registration with categories
Prowlarr Sonarr, Radarr, Lidarr Indexer sync (native app sync)
Prowlarr Bindery Indexer endpoints (manual — see below)
Root folders Every *arr Per media type, under /data/media
Sonarr, Radarr Bazarr Subtitle provider wiring
Jellyfin Seerr Identity source — one household account
Sonarr, Radarr Seerr Request fulfilment targets
Every service Homepage API keys for live widgets
Recyclarr Sonarr, Radarr Quality profile synchronisation

Bindery is wired differently. Prowlarr’s app sync supports a fixed set of Servarr applications and Bindery isn’t among them. It consumes Prowlarr’s Torznab endpoints instead, which works but must be configured explicitly rather than via the same sync path. A real asymmetry, specified rather than glossed.

This table is a promise, not an illustration (D1-R18). Every row of it is a connection the operator is entitled to have made for them, and a row nothing makes is a stack that starts, reports healthy, and quietly does not work — because each of these is exactly the step whose absence is invisible until somebody asks for something and nothing happens.

That is not hypothetical. Two of these rows were declared here and obliged by no acceptance criterion, and both turned out never to have worked: the request service’s identity was refused on every run it ever made, and the download client was refused by SABnzbd. Both had passing tests, because a test over a fake proves what the author believed the service wanted rather than what it wants. Neither had a criterion, so nothing counted them as missing.

A row added to this table therefore arrives already obliged, and one that cannot be made yet belongs in the prose as an asymmetry — the way Bindery does above — rather than sitting in the table unmade.

Jellyfin as household identity is wired unconditionally

Connecting Seerr’s authentication to Jellyfin is one API call and it’s the difference between a household member having one account or two. It is never optional.

The fulfilment targets decide what can be asked for

Seerr does not discover the *arrs; it is told about them. Until it is, a request reaches nothing — the household asks, the ask is accepted, and no downloader ever hears about it, which is the failure mode this whole feature exists to prevent.

The second half matters as much as the first. Seerr offers what its configured targets can deliver, so registering only the *arrs actually in the stack is what makes D4’s promise true: television is not offered where Sonarr is not running. Registering one that is absent would offer the household a thing that cannot arrive.

Idempotent, and drift-aware

Running seed twice changes nothing. Crucially, it does not re-assert values the operator has since changed — that’s C9, and it’s what makes seed safe to run on a stack someone has tuned.

Partial success is reported precisely

Some services will be unavailable — not in the active form, still starting, unhealthy. Seed wires what it can, reports exactly what it couldn’t and why, and is re-runnable to complete the rest. It does not fail wholesale because Lidarr isn’t running.

Verified, not assumed

After writing, each connection is read back. “Registered SABnzbd in Sonarr” means Sonarr was asked and confirmed it.

Fast enough to be routine

The target is under a minute. Seed being cheap is what makes configuration disposable — and disposable configuration is what makes the stack safe to experiment with.

On the companion

Seed makes the thirty connections, and the companion may run it (N7). That is safe from a phone for two reasons this feature already requires: a second run with nothing changed writes nothing (D1-R2), and a value the operator set is kept rather than overwritten (D1-R3). N7-R15 offers the run and shows each connection in the state it ended in; N7-R16 forbids offering to overwrite the operator’s value.

Partial success is reported precisely is the property that has to survive a small screen. N7-R1 requires what could not be carried shown with a reason for each, and not subordinated to a success message — twenty-eight connections of thirty is a success by any count, and the two that failed are the entire content of the report. Because seed is re-runnable, N7-R9 requires a run that carried nothing told apart from one that has not run; with no shell to check, those look identical.

Verified, not assumed is what gives the app something honest to render. Where a connection was read back it is sound; where it could not be read back, N7-R7 requires that shown as its own answer rather than as sound, because an unverifiable wiring rendered green is worse than showing nothing. Where the stack attaches a severity, N7-R8 requires both what would break and what would put it right — away from the machine, the second is the only half that can be acted on.

Idempotence is not rehearsal. Running seed twice changing nothing is a property of the operation; a run that deliberately writes nothing is a rehearsal, and N7-R10 holds it to the labelling N6-R1 requires everywhere else.

The request service reaches the *arrs through a gate

Seerr is household tier, and a key to Sonarr, Radarr or Jellyfin is administration of that service. So the connections Sonarr, Radarr → Seerr and Jellyfin → Seerr are made through the request gate (ADR-0032). Seerr is registered with each *arr at the gate’s route for it and holds a token lemonfiber minted for that route; the gate holds the *arr’s key (D1-R19). Once Seerr is initialised, lemonfiber speaks to it with Seerr’s own key, and the administrator’s password the initialising sign-in carried is rotated (D1-R20). A stack whose Seerr already holds the keys has them taken back, each move proved through Seerr before anything is revoked (D1-R21).

States

Per connection:

State Meaning
pending Not yet attempted
wired Written and read back successfully
already-wired Present and correct; no action taken
drifted Present but differing from lemonfiber’s baseline — preserved
skipped Prerequisite service unavailable
failed Attempted and rejected

Edge cases

Situation Behaviour
Service still starting Wait briefly, then skipped with a note that re-running will complete it.
Service in the stack but not the active form skipped, not failed. Expected and normal.
NZBHydra2 found without authentication Turn it on, keeping everything it holds; read the indexers back.
The operator turned NZBHydra2’s authentication off Keep it, report it as drift, and the doctor reports the exposure; a reset turns it back on.
The recorded NZBHydra2 password is missing or refused Report it as refused, naming the setting; never turn authentication off to get back in.
NZBHydra2’s health check reads a page authentication now guards The stack’s check reads a path NZBHydra2 answers without a credential, so turning authentication on never reads as unhealthy.
API key not yet generated Service hasn’t completed first start. Wait, then skip.
Service rejects a value Report the service’s own error message; don’t paraphrase it into something vaguer.
Root folder path doesn’t exist Create it if within the data root; refuse and explain if outside.
Operator changed a wired value Preserve it (C9); report as drift, don’t revert.
Download client already registered under a different name Detect by connection details, not by label. Don’t create duplicates.
Seed run before any content exists Fine. Wiring is independent of content.
Seerr already using local accounts Report the conflict; switching identity sources affects existing users, so it needs consent.
Two *arrs claiming the same root folder Refuse and explain — this causes import conflicts later.
Service upgraded with a changed API Detect the version and report unsupported rather than writing something malformed.
Seed interrupted partway Safe. Each connection is independent; re-running completes the rest.
Native-mode Jellyfin Wire via its host address; verify reachability first and report clearly if unreachable.

Acceptance criteria

ID Requirement
D1-R1 Service API keys MUST be read from service configuration, never requested from the operator.
D1-R2 Seed MUST be idempotent; a second run with no changes MUST make no writes.
D1-R3 Seed MUST NOT overwrite values that have drifted from lemonfiber’s baseline.
D1-R4 Each connection MUST be read back and verified after writing.
D1-R5 Unavailable prerequisites MUST produce skipped, not failed.
D1-R6 Partial completion MUST report exactly which connections were not made and why, and MUST be resumable by re-running.
D1-R7 Seerr MUST be configured to authenticate against Jellyfin.
D1-R8 Existing connections MUST be detected by connection details rather than label, to avoid duplicates.
D1-R9 Root folders MUST be created when within the data root, and refused with an explanation when outside it.
D1-R10 Two *arrs sharing a root folder MUST be refused with an explanation.
D1-R11 Service-reported errors MUST be surfaced verbatim rather than paraphrased.
D1-R12 An unsupported service API version MUST be reported rather than written to.
D1-R13 Interruption MUST leave every completed connection intact and valid.
D1-R14 A full seed against a healthy stack SHOULD complete within 60 seconds.
D1-R15 Bindery MUST be wired via Torznab endpoints, and the absence of Prowlarr app sync MUST be documented in-product.
D1-R16 Seeding MUST replace qBittorrent’s temporary WebUI password with a generated one and record it where the forwarded-port push reads it.
D1-R17 Each *arr that fulfils requests MUST be registered with the request service as a fulfilment target, and one absent from the stack MUST NOT be.
D1-R18 Every connection named in the wiring graph MUST be made where the services at both ends are in the stack, and each MUST be proven against the service that received it rather than against a stand-in for it.
D1-R19 The request service MUST reach each fulfilling *arr and Jellyfin only through the request gate: each *arr MUST be registered with it at the gate’s route for that *arr and with the token lemonfiber minted for that route, never with the *arr’s own key, and it MUST be left holding no Jellyfin API key and no Jellyfin administrator’s session (ADR-0032).
D1-R20 Once the request service is initialised, lemonfiber MUST authenticate to it with the request service’s own API key and MUST NOT send it the media server administrator’s password; the password the initialising sign-in carried MUST be rotated once that sign-in completes.
D1-R21 Where the request service is found holding a credential of an *arr or of Jellyfin, lemonfiber MUST move it to the request gate and prove the move through the request service before revoking anything, MUST then revoke or rotate every credential the request service held, and MUST report any consumer the rotation could not update.
D1-R22 Seeding MUST turn on NZBHydra2’s authentication, with an administrator lemonfiber names and a password it generates and records under that service’s own administrator-password setting, and MUST then prove that a read of its configuration or of the indexers it holds, presenting nothing, is refused.
D1-R23 Where NZBHydra2 is found running without authentication that lemonfiber has no record of having set, seeding MUST turn it on without removing or changing any indexer, setting or history the service holds, and MUST prove after the change that every indexer it held before is still held.
D1-R24 Where authentication lemonfiber turned on in NZBHydra2 has since been turned off or changed, seeding MUST keep it and report it as drift, and a reset MUST turn it back on.
D1-R25 Where the password lemonfiber recorded for NZBHydra2 is missing or refused, seeding MUST report the connection as refused, naming the setting, and MUST NOT turn the service’s authentication off or replace its administrator.
D1-R26 The doctor MUST report an NZBHydra2 that answers a read of its configuration to a caller presenting nothing, naming the service, what the read exposes, and that a seed or a reset turns its authentication on.