-
Notifications
You must be signed in to change notification settings - Fork 0
Developer Guide
For people scripting against CHUB, extending it, or auditing it. If you're just running CHUB, start with the User Guide instead.
-
REST API reference — every
/api/*endpoint. A live Swagger UI also exists at/docson any running CHUB instance. - Repo layout + local dev (below)
- Writing a new module (below)
- Security internals (below)
backend/
api/ FastAPI routers — one file per resource
modules/ Scheduled/on-demand modules — one file each
util/ Config, auth, logging, rate limiting, path validation
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, audit 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 /api-
ruff check— Python lint -
pytest tests/— backend tests -
npm run lintinfrontend/— JS lint -
npm run buildinfrontend/— 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(): self.logger.info("cancelled") return self.process(item)
-
Add a Pydantic config model to
backend/util/config.pyand attach it toChubConfig:class MyModuleConfig(BaseModel): log_level: str = "info" dry_run: bool = False # ... your fields ... 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
-
Rebuild the container (or restart the dev server). The scheduler, job processor, and UI pick up the new module automatically.
Cancellation contract: check self.is_cancelled() inside any long-running loop. When a user hits the cancel endpoint, it flips a threading event the base class tracks — your module is responsible for noticing and returning.
End-user security advice lives in Configuration and the FAQ. This section is the engineering-level picture — what each guard actually does and where to find it.
- Passwords hashed with
bcrypt(cost factor 12), stored inconfig.ymlunderauth.password_hash. - JWTs signed with a per-install random
jwt_secret(regenerated on--reset-auth). -
AuthMiddlewareinbackend/api/main.pyvalidates every/api/*request. Exempt prefixes (no JWT required):/api/auth/,/api/health,/api/version, plus static paths (/assets/,/icons/,/img/,/posters/). - Webhooks (
/api/webhooks/*) don't use JWT — they gate on the optionalgeneral.webhook_secretvia theverify_webhook_secretFastAPI dependency inbackend/api/webhooks.py. - EventSource streams can't send custom headers, so
/api/modules/eventsaccepts?token=<jwt>as a fallback.
-
backend/util/rate_limiter.py— token bucket. - Login limiter:
rate=0.2, burst=5(one attempt per 5 seconds, burst of 5). Excess returns429.
- Applied on outbound probes to ARR/Plex instance URLs (
backend/util/ssrf.pyor the instances helper). - Rejects: reserved IPv4/IPv6 ranges, link-local, multicast, cloud-metadata hosts (
169.254.169.254,metadata.google.internal), non-http(s)schemes, unresolvable hostnames. - Returns a clear error in the UI — the instance test fails with "blocked" rather than silently hanging.
- Applied to path-valued config fields that get passed to subprocesses or shell-adjacent tools (notably
hash_databaseinjduparrandsync_location,gdrive_sa_location, folder IDs insync_gdrive). - Rejects: null bytes, values starting with
-(would be interpreted as a CLI flag), paths outside the configured allowed roots.
- Optional shared secret via
general.webhook_secret. If set, every/api/webhooks/*call must sendX-Webhook-Secret: <secret>or?secret=<secret>. - Duplicate detection: SHA-256 of
(title, year, tmdb_id, tvdb_id, imdb_id, event_type)— identical payloads within 5 seconds are silently debounced.
-
SmartRedactionFilterscrubs JWTs, Bearer tokens,X-Api-Key,X-Plex-Token, OAuth secrets, Discord/generic webhook URLs, AWS keys, and GitHub tokens before they're written to disk. - Redaction happens at the logging layer — modules don't need to remember to scrub.
-
GET /api/configredacts these fields to********in the response:api,api_key,access_token,refresh_token,token,client_secret,password_hash,jwt_secret,webhook_secret. - When the UI saves config back, any field still equal to
********is replaced with the current on-disk value — so you can edit non-sensitive fields in the UI without re-entering API keys.
- 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.