Houndarr architecture reference
SkillFiles & storageHoundarr'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.
No other account needed.
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-databaseskill (loads onsrc/houndarr/database.py). - Algorithmic verification: see
verify-algorithmsskill (loads onsrc/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;SystemStatusandQueueStatusbackArrClient.ping()andArrClient.get_queue_status(); per-app*WantedEpisode/*WantedMovie/*WantedAlbum/*WantedBookand*LibraryEpisode/*LibraryMovie/*LibraryAlbum/*LibraryBookmodels name the record shapes.ArrSeries/ArrArtist/ArrAuthortype the parent-aggregate fetches. All wire models extend an internal_ArrModelthat setspopulate_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 usesslots=True.Instancecomposes seven frozen sub-structs (core,missing,cutoff,upgrade,schedule,snapshot,timestamps) and is itself frozen and slotted; callers evolve it throughdataclasses.replace.AppSettingsis 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-leveldispatch()sends the request straight to_dispatch_api_key, which verifiesX-Api-Keyagainst thewidget_api_keytable (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_builtinand_dispatch_proxyshort-circuits these before any session or proxy-header check.- Everything else: mode-dependent.
_dispatch_builtinenforces the session cookie + CSRF (default);_dispatch_proxyenforces the proxy-trust gate + CSRF whenHOUNDARR_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