Configuration¶
The API reads its configuration from environment variables (prefix APP_), the worker
from SNATCH_*. Everything else is stored in Postgres and edited through the API or the
UI.
API (snatcharr)¶
| Variable | Default | Meaning |
|---|---|---|
APP_DB_DSN |
required | Postgres DSN, e.g. postgres://snatcharr:secret@postgres:5432/snatcharr?sslmode=disable |
APP_HTTP_ADDR |
:8080 |
HTTP listen address (API, SSE, embedded UI) |
APP_GRPC_LISTEN |
:9090 |
gRPC listen address for workers |
APP_GRPC_KEEPALIVE_MAX_CONNECTION_AGE |
2m |
After this age a worker connection takes no new RPCs and new ones move to a fresh connection; negative = never |
APP_GRPC_KEEPALIVE_MAX_CONNECTION_AGE_GRACE |
unlimited | How long RPCs already running on an aged connection may finish. A finite value must be at least 35s (the 30 s LeaseRun long-poll plus margin) or the API refuses to start; note that a finite value can still cut a run's ReportEvents stream |
APP_CRYPTO_KEY |
required in prod | Key that encrypts *arr API keys and download-client secrets at rest (openssl rand -hex 32). The dev profile without it runs on a development key published in the source and warns that stored secrets are unprotected; prod refuses to start on that key |
APP_HTTP_CSRF_SECRET |
not read | No longer used: the API refuses cross-site requests from the browser's Sec-Fetch-Site and Origin headers, which needs no secret and cannot be switched off. A value that is still set is ignored |
APP_LEADER_ENABLED |
false |
Elect one replica (Postgres advisory lock) to run the planner, Seerr sync and Configarr poll; leave off for a single replica |
APP_LEADER_NAME |
snatcharr |
Lock name; one per deployment |
APP_LEADER_IDENTITY |
hostname | This replica's name in logs |
APP_LEADER_PG_RETRY |
2s |
How often a follower retries the lock |
APP_SNATCHARR_PROFILE |
prod |
prod or dev; dev relaxes cookie security and uses the built-in dev key |
APP_SNATCHARR_PUBLIC_URL |
External base URL (cookies, links) | |
APP_SNATCHARR_WORKER_TOKEN |
Shared bearer the worker presents over gRPC | |
APP_SNATCHARR_SESSION_TTL |
8h |
Cookie session lifetime |
APP_SNATCHARR_SESSION_SECURE |
true |
Secure flag on the session cookie; while it is on, responses also carry Strict-Transport-Security (one year), so leave it on only when browsers reach SnatchArr over HTTPS |
APP_SNATCHARR_APIKEY_SECRET |
required in prod | HMAC secret for API keys |
APP_SNATCHARR_CONFIGARR_CONFIG |
Path to Configarr config.yml (linked mode) |
|
APP_SNATCHARR_CONFIGARR_DOWNLOAD_CLIENTS |
Path to download clients YAML file (linked mode) | |
APP_SNATCHARR_CONFIGARR_SECRETS |
Path to Configarr secrets.yml |
|
APP_SNATCHARR_CONFIGARR_WATCH |
true |
Re-import on file change |
APP_SNATCHARR_PROWLARR_LIMIT_THRESHOLD |
0.9 |
Fraction (0, 1] of an indexer's Prowlarr query limit at which SnatchArr treats the indexer as exhausted (Prowlarr) |
APP_SNATCHARR_LOGS_RING_SIZE |
2000 |
Log records the API keeps in memory for the admin Logs page; clamped to 100 to 20000 |
APP_SNATCHARR_WEB_DEV |
false |
Disable the embedded SPA (Vite dev server proxies instead) |
APP_SNATCHARR_PROXY_TRUSTED |
Comma-separated CIDRs of trusted reverse proxies (e.g. 10.0.0.0/8). Enables client IP resolution from X-Forwarded-For for login throttling |
|
APP_SNATCHARR_SETUP_SECRET |
Optional bootstrap secret required by POST /auth/setup. Generated by default in Helm chart; empty allows setup without a secret until the first admin user is created |
|
APP_SNATCHARR_OIDC_ENABLED |
false |
Enable OIDC SSO (Single Sign-On) |
APP_SNATCHARR_OIDC_ISSUER_URL |
OIDC issuer URL (https, or http in dev profile) | |
APP_SNATCHARR_OIDC_CLIENT_ID |
OAuth2 client ID | |
APP_SNATCHARR_OIDC_CLIENT_SECRET |
OAuth2 client secret | |
APP_SNATCHARR_OIDC_REDIRECT_URL |
OAuth2 callback URL (defaults to public.url + /api/v1/auth/oidc/callback) |
|
APP_SNATCHARR_OIDC_SCOPES |
openid,email,profile |
Comma-separated scopes (openid always present) |
APP_SNATCHARR_OIDC_ALLOWED_SUBJECTS |
Comma-separated allowed IdP sub claims |
|
APP_SNATCHARR_OIDC_ALLOWED_EMAILS |
Comma-separated allowed emails (email_verified==true required) |
|
APP_SNATCHARR_OIDC_DISCOVERY_TIMEOUT |
10s |
IdP discovery timeout |
APP_SNATCHARR_PROXY_AUTH_ENABLED |
false |
Enable trusted proxy-header auth (Single Sign-On) |
APP_SNATCHARR_PROXY_AUTH_HEADER |
Remote-User |
Name of header containing authenticated username |
APP_SNATCHARR_PROXY_AUTH_TRUSTED_CIDRS |
Comma-separated CIDRs of reverse proxies trusted for proxy-header auth | |
APP_SNATCHARR_PROXY_AUTH_ALLOWED_USERS |
Comma-separated allowed usernames | |
APP_OTEL_ENABLED |
true |
OpenTelemetry export (no-op without an endpoint) |
APP_LOG_FORMAT |
json |
json or text |
Worker (snatch-worker)¶
| Variable | Default | Meaning |
|---|---|---|
SNATCH_API_GRPC |
http://snatcharr:9090 |
API gRPC endpoint |
SNATCH_WORKER_TOKEN |
Bearer token (must match APP_SNATCHARR_WORKER_TOKEN) |
|
SNATCH_CONCURRENCY |
2 |
Snatches executed in parallel |
RUST_LOG |
info |
Log filter |
The worker never holds state. It leases a snatch, heartbeats while working, reports events and completes; a restart loses nothing.
Snatch policy¶
Every instance has one policy (GET/PUT /api/v1/instances/{id}/policy).
| Field | Default | Range |
|---|---|---|
missing_per_cycle |
1 | 0 to 100 (0 disables) |
upgrade_per_cycle |
0 | 0 to 100 |
cycle_interval_s |
900 | at least 60 |
hourly_cap (stamina) |
20 | 1 to 500 |
selection |
random |
random, sequential, recent |
monitored_only |
true | |
skip_future_releases |
true | |
radarr_release_type |
physical |
physical, digital, cinema |
sonarr_missing_mode |
episodes |
episodes, season_packs, shows |
sonarr_upgrade_mode |
episodes |
episodes, season_packs |
lidarr_missing_mode |
artist |
artist, album |
processed_ttl_h (afterglow) |
168 | 1 to 8760 |
max_queue_size |
-1 | -1 disables the *arr queue ceiling |
await_command |
false | Poll the search command until it completes (5 min max) |
page_size |
100 | 10 to 1000 |
recent_grab_window_h |
24 | 0 to 720; items grabbed this recently (and everything queued) are skipped |
afterglow_max_h |
720 | 1 to 8760; cap for the doubling afterglow |
include_tags |
empty | up to 100 *arr tag ids; when not empty, only items with one of these tags |
exclude_tags |
empty | up to 100 *arr tag ids; items with any of these tags are skipped; an id may not be in both tag lists |
include_quality_profiles |
empty | up to 100 *arr quality profile ids; when not empty, only items using one of these profiles |
exclude_quality_profiles |
empty | up to 100 *arr quality profile ids; items using any of these are skipped; an id may not be in both profile lists |
release_delay_h |
0 | 0 to 8760; skip items released less than this many hours ago (0 = off; items without a date are kept) |
dry_run |
false | When true, scheduled and periodic runs only preview without dispatching searches |
skip_unavailable_movies |
true | Radarr only: skip movies whose minimum availability is not reached (isAvailable false); they cost no stamina, and the run's candidates_filtered event counts them. Movies without the flag are searched |
An episode, album or book has the tags and quality profile of its series, artist or author.
GET /api/v1/instances/{id}/policy/filter-options lists the tags and quality profiles the
instance offers (admins only); the policy form picks from that list.
Queue policy¶
Each instance has a queue policy (GET/PUT /api/v1/instances/{id}/queue-policy; PUT is
admin-only). When enabled is true, the elected replica sweeps the instance's *arr download
queue and counts consecutive strikes per download. GET /api/v1/instances/{id}/queue-strikes
lists the downloads that hold a strike now; downloads the last sweep found clean are left out.
The rules and their order are described in ADR-0012.
Every rule starts in observe mode: a strike is recorded and shown, and the queue is left
alone. A rule in act mode removes the download once its strike count reaches the threshold;
every rule except unwanted_unmonitored also blocklists the release so the *arr searches again.
Whisparr has no queue API in goenvoy and is skipped.
Rules¶
| Rule | Matches when | Default threshold |
|---|---|---|
stalled |
Downloading, and the remaining size did not shrink since the last sweep | 6 |
slow |
Downloading slower than min_bytes_per_second since the last sweep |
6 |
failed |
Queue status or tracked state is failed | 2 |
import_blocked |
The *arr cannot import the finished download | 6 |
unwanted_score |
Custom format score below min_custom_format_score |
1 |
unwanted_unmonitored |
The episode, movie, album or book is no longer monitored | 1 |
Queued, paused and delayed downloads, downloads being imported, and downloads younger than the grace period are never evaluated.
Policy fields¶
| Field | Default | Range | Meaning |
|---|---|---|---|
enabled |
false |
bool | Sweep this instance's queue |
grace_seconds |
900 | 0 to 86400 | Minimum age before any rule looks at a download (the UI edits it in minutes) |
min_bytes_per_second |
0 | >= 0 | Floor for slow; 0 turns slow off |
min_custom_format_score |
unset | integer | Floor for unwanted_score; unset turns unwanted_score off |
max_removals_per_sweep |
10 | 1 to 100 | Most downloads one sweep may remove |
Runtime settings¶
GET/PUT /api/v1/settings:
| Field | Default | Meaning |
|---|---|---|
history_retention_days |
90 | 1 to 3650; how long events and finished runs are kept. The elected replica deletes older ones once an hour |
user_agent |
SnatchArr/1.0 (...) |
Sent to every *arr and download client |
global_hourly_cap |
0 | Stamina shared by every instance (0 = off) |
The same hourly pass also forgets afterglow entries 90 days after they expired, spent
stamina buckets after two hours, expired login sessions and workers that have not called
for history_retention_days. It deletes at most 100,000 rows
per table per pass and logs one retention: purge pass line with the counts. A larger
backlog shrinks over the following hours.
Configarr¶
Point APP_SNATCHARR_CONFIGARR_CONFIG (and _SECRETS) at your Configarr files and every
sonarr, radarr, lidarr, readarr (legacy) and whisparr instance is
imported on start and kept in sync: the files are polled every 30 seconds (inotify misses Kubernetes ConfigMap
swaps, polling does not). Only base_url, api_key and the enabled flags are read;
!secret, !env and !file values are resolved like Configarr does. Whisparr entries are
probed to tell v2 from v3. An instance that disappears from the file is switched off, not
deleted. Imported instances are read-only in SnatchArr except for their snatch policy. Deleting
one that the file still declares, enabled or not, is refused with 403; once it is gone from the
file and switched off, it can be deleted.
POST /api/v1/configarr/import runs a one-shot import (with config_path and
secrets_path in the body, or the linked files); GET /api/v1/configarr/status shows
linked mode and the last result.
Declarative Download Clients¶
When APP_SNATCHARR_CONFIGARR_DOWNLOAD_CLIENTS points to a YAML file, download clients are declaratively provisioned and kept in sync alongside instances:
qbittorrent: # section = domain.DownloadClientKind value; accept every kind ParseDownloadClientKind accepts
main: # entry name -> key "qbittorrent/main", Name "main"
base_url: http://qbittorrent.media.svc:8080
username: !env QBIT_USER
password: !file /etc/snatcharr/configarr-secrets/qbit-password
instance: sonarr/main # optional Configarr instance key; omitted = applies to every instance
max_active: 0 # optional int
bandwidth_budget_bps: 0 # optional int64
enabled: true # optional, default true
sabnzbd:
main:
base_url: http://sabnzbd.media.svc:8080
api_key: !secret sabnzbd_api_key # secret = api_key or password, exactly one, both -> parse error
!secret (resolved from APP_SNATCHARR_CONFIGARR_SECRETS), !env and !file tags work identically to instance secrets. When an entry disappears from the file, it is pruned. Provisioned clients are marked with source configarr; mutating or deleting them via the UI or API is rejected with 403 Forbidden.