Houndarr architecture reference

SkillFiles & storage

Houndarr's source layout and architectural patterns at file granularity. Loads when reading or editing src/houndarr/. Covers the per-file purpose for auth/, clients/, engine/, routes/, services/; the wire-models vs domain-models split; the *arr API spec snapshots under docs/api/; and pointers to the more specific database / engine skills for narrower scopes.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Houndarr architecture reference skill

What this skill tells your AI

The instructions your AI receives, as published by av1155/houndarr in .agents/skills/houndarr-architecture/SKILL.md and read by ahel’s review.

For narrower scopes:

  • Database schema and migrations: see houndarr-database skill (loads on src/houndarr/database.py).
  • Algorithmic verification: see verify-algorithms skill (loads on src/houndarr/engine/**).

Source layout

src/houndarr/
  __main__.py          # CLI entry point (Click), logging setup, uvicorn.run
  app.py               # create_app(), lifespan, middleware registration
  auth/                # AuthMiddleware, bcrypt, CSRF, rate limiter (seam package)
    password.py        # bcrypt verify / hash helpers
    rate_limit.py      # in-memory login rate limiter
    session.py         # signed session cookie encode / decode
    setup.py           # first-run admin setup + password policy
    csrf.py            # CSRF double-submit token rotation
    proxy_auth.py      # reverse-proxy trust gate and header extraction
    identity.py        # current-user resolution from session or proxy header
    middleware.py      # AuthMiddleware dispatch (builtin vs proxy path)
  config.py            # AppSettings dataclass, get_settings() singleton
  crypto.py            # Fernet encrypt/decrypt, master key management
  database.py          # get_db() context manager, schema migrations
  enums.py             # StrEnum consolidation (SearchKind, SearchAction, CycleTrigger, ItemType)
  errors.py            # HoundarrError hierarchy (Client/Engine/Service/Route)
  value_objects.py     # Frozen value objects shared across layers (ItemRef)
  clients/             # httpx-based *arr API clients
    base.py            # ArrClient ABC with _get()/_post() + raise_for_status() + get_queue_status()
    sonarr.py          # SonarrClient (episode/season search, v3 API)
    radarr.py          # RadarrClient (movie search, v3 API)
    lidarr.py          # LidarrClient (album/artist search, v1 API)
    readarr.py         # ReadarrClient (book/author search, v1 API)
    whisparr_v2.py     # WhisparrV2Client (Sonarr-based, episode/season search)
    whisparr_v3.py     # WhisparrV3Client (v3, Radarr-based, movie/scene search)
  engine/
    candidates.py      # SearchCandidate dataclass, ItemType re-export, date helpers
    search_loop.py     # run_instance_search(): unified search pipeline (missing/cutoff/upgrade passes, queue-backpressure gate)
    supervisor.py      # Supervisor: one asyncio.Task per enabled instance
    adapters/
      __init__.py      # AppAdapter dataclass, ADAPTERS registry, get_adapter()
      protocols.py     # AppAdapterProto: runtime_checkable Protocol matching the AppAdapter shape
      sonarr.py        # Sonarr adapter: candidate conversion + dispatch
      radarr.py        # Radarr adapter: candidate conversion + dispatch
      lidarr.py        # Lidarr adapter: candidate conversion + dispatch
      readarr.py       # Readarr adapter: candidate conversion + dispatch
      whisparr_v2.py   # Whisparr v2 adapter: candidate conversion + dispatch
      whisparr_v3.py   # Whisparr v3 adapter: movie/scene candidate conversion + dispatch
  routes/
    _htmx.py           # is_hx_request() shared helper for partial vs full renders
    pages.py           # Setup, Login, Dashboard, Logs, Settings page routes
    health.py          # GET /api/health (Docker HEALTHCHECK)
    settings/          # Settings surface split by concern
      __init__.py      # composes the sub-routers into a single settings_router
      _helpers.py      # template render, client build, connection check, validators
      page.py          # GET /settings
      account.py       # POST /settings/account/password
      instances.py     # /settings/instances/* (CRUD, test-connection, toggle)
    api/
      logs.py          # GET /api/logs (JSON, with cursor-based pagination)
      status.py        # GET /api/status (JSON, dashboard polling)
  services/
    instances.py       # Instance CRUD, InstanceType StrEnum
    cooldown.py        # Per-item search cooldown tracking
    url_validation.py  # SSRF guard for instance URLs

Wire models vs domain models

  • Wire models (clients/_wire_models/): every *arr HTTP response is validated with a Pydantic model from this package before it reaches a parser. PaginatedResponse[T] (generic, PEP 695 syntax) covers the shared /wanted/* envelope; SystemStatus and QueueStatus back ArrClient.ping() and ArrClient.get_queue_status(); per-app *WantedEpisode / *WantedMovie / *WantedAlbum / *WantedBook and *LibraryEpisode / *LibraryMovie / *LibraryAlbum / *LibraryBook models name the record shapes. ArrSeries / ArrArtist / ArrAuthor type the parent-aggregate fetches. All wire models extend an internal _ArrModel that sets populate_by_name=True + extra="ignore" so unknown fields from new *arr versions never raise. Field names are snake_case in Python and alias to the camelCase the APIs serialise.
  • Domain models (parsed result types): MissingEpisode, LibraryMovie, etc. are frozen dataclasses, one per client file next to the client that builds them. Every frozen dataclass uses slots=True. Instance composes seven frozen sub-structs (core, missing, cutoff, upgrade, schedule, snapshot, timestamps) and is itself frozen and slotted; callers evolve it through dataclasses.replace. AppSettings is the only deliberately-mutable dataclass (env overrides applied in-place on the lazy singleton).

Auth composition

Global AuthMiddleware (Starlette BaseHTTPMiddleware) routes every request through one of three path buckets; no per-route auth decorators:

  • _API_KEY_PATHS (currently /api/v1/widget): the top-level dispatch() sends the request straight to _dispatch_api_key, which verifies X-Api-Key against the widget_api_key table (constant-time compare on the SHA-256 digest) and applies a per-IP attempt rate limit. Bypasses session and CSRF.
  • _PUBLIC_PATHS (/setup, /login, /api/health, /static): no auth. Each of _dispatch_builtin and _dispatch_proxy short-circuits these before any session or proxy-header check.
  • Everything else: mode-dependent. _dispatch_builtin enforces the session cookie + CSRF (default); _dispatch_proxy enforces the proxy-trust gate + CSRF when HOUNDARR_AUTH_MODE=proxy.

Proxy-auth trust and header reads flow through two primitives in auth.py: _is_trusted_proxy(request) (IP gate) and _extract_proxy_username(request) (header read, assumes trust already verified). The middleware's _dispatch_proxy and the standalone _validate_proxy_auth both compose these so the gate logic lives in one place.

Encryption

Master key in request.app.state.master_key; passed explicitly to service functions as master_key= kwarg; never imported globally.

HTMX

SPA-like shell navigation; nav links use hx-target="#app-content" with hx-swap="innerHTML" and hx-push-url="true". Routes check is_hx_request(request) from routes/_htmx.py and return either partial or full template. Templates are lazily initialised via a module-level singleton.

Supervisor

One asyncio.Task per enabled instance; 10s shutdown timeout.

search_log

Every search attempt writes a row with action searched / skipped / error / info.

*arr API reference (local)

Full upstream OpenAPI specs vendored under docs/api/ (one per app: sonarr, radarr, whisparr_v2, whisparr_v3, lidarr, readarr). Source of truth when touching clients/ code; see docs/api/README.md. Refreshed weekly (Mon 10:00 UTC) by api-snapshot-refresh.yml, so specs are never more than a week stale.

Signals

GitHub stars
283
Forks
4
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
houndarr-architecture
Source
github.com/av1155/houndarr