Skip to content

Developer Guide

chodeus edited this page Apr 20, 2026 · 2 revisions

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 /docs on any running CHUB instance.
  • Repo layout + local dev (below)
  • Writing a new module (below)
  • Security internals (below)

Repo layout

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

Running locally

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

Tests & linters

  • ruff check — Python lint
  • pytest tests/ — backend tests
  • npm run lint in frontend/ — JS lint
  • npm run build in frontend/ — production bundle sanity check

Writing a new module

  1. 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)
  2. Add a Pydantic config model to backend/util/config.py and attach it to ChubConfig:

    class MyModuleConfig(BaseModel):
        log_level: str = "info"
        dry_run: bool = False
        # ... your fields ...
    
    class ChubConfig(BaseModel):
        ...
        my_module: MyModuleConfig = Field(default_factory=MyModuleConfig)
  3. Register the class in backend/modules/__init__.py:

    from backend.modules.my_module import MyModule
    MODULES["my_module"] = MyModule
  4. 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.


Security internals

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.

Authentication

  • Passwords hashed with bcrypt (cost factor 12), stored in config.yml under auth.password_hash.
  • JWTs signed with a per-install random jwt_secret (regenerated on --reset-auth).
  • AuthMiddleware in backend/api/main.py validates 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 optional general.webhook_secret via the verify_webhook_secret FastAPI dependency in backend/api/webhooks.py.
  • EventSource streams can't send custom headers, so /api/modules/events accepts ?token=<jwt> as a fallback.

Rate limiting

  • backend/util/rate_limiter.py — token bucket.
  • Login limiter: rate=0.2, burst=5 (one attempt per 5 seconds, burst of 5). Excess returns 429.

SSRF guard

  • Applied on outbound probes to ARR/Plex instance URLs (backend/util/ssrf.py or 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.

Path-safety / argument-smuggling guard

  • Applied to path-valued config fields that get passed to subprocesses or shell-adjacent tools (notably hash_database in jduparr and sync_location, gdrive_sa_location, folder IDs in sync_gdrive).
  • Rejects: null bytes, values starting with - (would be interpreted as a CLI flag), paths outside the configured allowed roots.

Webhook auth

  • Optional shared secret via general.webhook_secret. If set, every /api/webhooks/* call must send X-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.

Log redaction

  • SmartRedactionFilter scrubs 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.

Secret handling in the API

  • GET /api/config redacts 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.

Contributing

  1. Open an issue first for anything non-trivial so scope can be agreed.
  2. Fork, branch, PR against main.
  3. Run the linters and build the frontend before pushing.
  4. Keep PRs focused — one feature per PR.

Patch-level fixes can skip the issue step.

Clone this wiki locally