Skip to content

Developer Guide

chodeus edited this page Jun 17, 2026 · 4 revisions

🏠 Home › Developer Guide

Developer Guide

For people scripting against CHUB, extending it, or auditing it. If you're just running CHUB, start with the User Guide.

On this page

Architecture

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"]
Loading

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 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

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
  • python3 -m pytest — backend tests
  • npm run lint (in frontend/) — JS lint
  • npm run build (in frontend/) — production bundle sanity check

Writing a 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():
                    return
                self.process(item)
  2. Add a Pydantic config model in backend/util/config.py and attach it to ChubConfig:

    class MyModuleConfig(BaseModel):
        log_level: str = "info"
        dry_run: bool = False
    
    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. 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).

Extensions

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.

Security guards

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 returns 429.
  • SSRF guardbackend/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 safetybackend/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_secret via X-Webhook-Secret header 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 redactionGET /api/config replaces secret leaf keys with ******** (matched by exact key name); on save, fields still equal to ******** keep their on-disk value.

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.


Next: REST API · Related: Configuration, Modules

Clone this wiki locally