-
Notifications
You must be signed in to change notification settings - Fork 1
Developer Guide
🏠 Home › Developer Guide
For people scripting against CHUB, extending it, or auditing it. If you're just running CHUB, start with the User Guide.
The React frontend talks to FastAPI over /api. The API drives the modules, which the scheduler also fires on cron/interval. State lives in SQLite plus config.yml. Modules reach out to your media stack.
flowchart LR
UI["Browser<br/>(React + Vite)"] -->|/api| API["FastAPI<br/>(backend/api)"]
API --> MOD["Modules<br/>(backend/modules)"]
SCHED["Scheduler"] --> MOD
API --> DB[("SQLite + config.yml")]
MOD --> DB
MOD --> EXT["Radarr · Sonarr · Lidarr<br/>Plex · Google Drive<br/>fanart.tv · TMDB"]
backend/
api/ FastAPI routers — one file per resource
modules/ Scheduled/on-demand modules — one file each
util/ Config, auth, logging, rate limiting, path safety, SSRF guard
extensions/ Self-registering develop-only tools
frontend/
src/ React 19 app, bundled by Vite
main.py Entry point — starts scheduler, worker, HTTP server
config.yml Runtime config, validated by Pydantic on load
chub.db SQLite — users, jobs, media cache, history, health snapshots
git clone https://github.com/chodeus/chub.git
cd chub
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python3 main.py # backend on :8000
cd frontend
npm install
npm run dev # Vite dev server on :5174, proxies /apiTests & linters
-
ruff check .— Python lint -
python3 -m pytest— backend tests -
npm run lint(infrontend/) — JS lint -
npm run build(infrontend/) — production bundle sanity check
-
Create
backend/modules/my_module.py:from backend.util.base_module import ChubModule class MyModule(ChubModule): def run(self): self.logger.info("starting") for item in self.work(): if self.is_cancelled(): return self.process(item)
-
Add a Pydantic config model in
backend/util/config.pyand attach it toChubConfig:class MyModuleConfig(BaseModel): log_level: str = "info" dry_run: bool = False class ChubConfig(BaseModel): my_module: MyModuleConfig = Field(default_factory=MyModuleConfig)
-
Register the class in
backend/modules/__init__.py:from backend.modules.my_module import MyModule MODULES["my_module"] = MyModule
-
Restart the dev server (or rebuild the container). The scheduler, job processor, and UI pick up the module automatically.
Cancellation contract. Check self.is_cancelled() inside any long-running loop. The cancel endpoint flips a threading event the base class tracks; your module must notice and return.
Progress reporting. For multi-minute phases, call self._report_progress(pct) inside the loop so the Jobs page percentage moves. set_job_context(job_id, db) is called before run(), so the helper is wired automatically. See poster_renamerr.py for a multi-phase example.
Heartbeat logging. For lines that fire repeatedly with no new info (per-item ticks), use self.logger.heartbeat(msg) instead of self.logger.info(msg). These get a [hb] marker the Logs page hides by default.
One-shot migrations. When you change a column's meaning, add a migration in backend/util/database/schema.py via self._apply_once(conn, name, sql, requires_table=...). The name is recorded so it runs once. Convention: ISO-date + snake_case (20260522_null_has_content_default_rows).
Develop-only tools (e.g. the CL2K poster maker) self-register from their own files under backend/extensions/<name>/ and frontend/src/extensions/<name>/ — they never edit shared files. Extension code lives on the develop branch only and never reaches main. See backend/extensions/__init__.py for the registration hook.
Engineering-level picture of each guard and where it lives.
-
Auth — bcrypt password hashes + per-install JWT secret;
AuthMiddleware(backend/api/main.py) validates every/api/*request. Exempt:/api/auth/,/api/health,/api/version, static paths. EventSource streams accept?token=<jwt>. -
Rate limiting — token bucket (
backend/util/rate_limiter.py); login limiter ~1 attempt / 5s, burst 5, excess returns429. -
SSRF guard —
backend/util/ssrf_guard.py; rejects reserved/link-local/multicast IPs, cloud-metadata hosts, non-http(s)schemes, and unresolvable hosts on outbound instance probes. -
Path safety —
backend/util/path_safety.py; path-valued config (e.g.jduparr.hash_database, sync_gdrive locations) cannot contain null bytes, start with-, or escape the allowed roots. -
Webhook auth — optional
general.webhook_secretviaX-Webhook-Secretheader or?secret=; duplicate payloads are debounced. - Log redaction — a logging filter scrubs JWTs, Bearer tokens, API/Plex tokens, OAuth secrets, and webhook URLs before they hit disk.
-
API secret redaction —
GET /api/configreplaces secret leaf keys with********(matched by exact key name); on save, fields still equal to********keep their on-disk value.
- Open an issue first for anything non-trivial so scope can be agreed.
- Fork, branch, PR against
main. - Run the linters and build the frontend before pushing.
- Keep PRs focused — one feature per PR.
Patch-level fixes can skip the issue step.
Next: REST API · Related: Configuration, Modules