Skip to content

Architecture

ChannelWatch follows a component-based architecture where each concern is isolated into a distinct layer. The six components communicate through well-defined interfaces, which makes it straightforward to add new notification providers without touching unrelated code.

graph TD
DVR["Channels DVR Server\n(SSE event stream)"]
BE["Core Backend\nMulti-DVR event monitor"]
AS["Alert System\nEvent processors"]
NS["Notification System\nProvider dispatch"]
WUI["Web UI\nNext.js + FastAPI"]
CS["Configuration System\nsettings.json + SQLite"]
EF["Extension Framework\nPlugin loader"]
DVR -->|"SSE /dvr/events"| BE
BE -->|"parsed events"| AS
AS -->|"alert payloads"| NS
NS -->|"HTTP delivery"| Providers["Pushover / Discord /\nTelegram / Slack /\nEmail / Gotify /\nMatrix / Custom Apprise"]
WUI -->|"read/write config"| CS
CS -->|"settings"| BE
CS -->|"settings"| AS
CS -->|"credentials"| NS
EF -->|"registers providers"| NS
WUI -->|"diagnostics API"| BE

The Core Backend connects to the Channels DVR server-sent event (SSE) stream at /dvr/events and processes the raw event feed. In v0.9.10 it uses an asyncio entry point and per-DVR task groups, while parts of event monitoring still run through internal threads. The practical result is one ChannelWatch container can monitor multiple DVR servers concurrently, with each DVR handled independently when it disconnects or reconnects.

Key responsibilities:

  • Maintain a persistent SSE connection to each configured DVR server
  • Parse raw event payloads into typed event objects
  • Route events to the Alert System for processing
  • Expose a health endpoint (/healthz/live, /healthz/ready, /healthz/startup) for Kubernetes probes
  • Write activity history to the SQLite database at /config/channelwatch.db

The Web UI is a Next.js frontend backed by a FastAPI server running inside the same container. It serves the dashboard at port 8501 and provides:

  • Real-time stream status and disk space overview
  • DVR server management (add, edit, soft-delete with 30-day undo)
  • Alert toggle controls per event type per DVR
  • Notification provider configuration with masking in the UI and sanitized exports
  • Diagnostics panel with connection tests and test-send buttons
  • Delivery log for reviewing recent notification history

The UI communicates with the Core Backend through the FastAPI layer. It does not write directly to the SQLite database.

The Alert System receives parsed events from the Core Backend and decides whether to fire a notification. Each alert type is an independent module:

Alert moduleTriggers on
Channel WatchingLive TV stream start / end
VOD WatchingOn-demand playback start (one notification per session)
Recording EventsScheduled, started, completed, cancelled, stopped
Disk SpaceFree space drops below warning or critical threshold

The Alert System handles session deduplication (preventing duplicate notifications for the same viewing session), cooldown enforcement for disk alerts, and per-DVR event isolation so a recording on DVR-A never triggers a notification routed to DVR-B's provider.

The Notification System receives alert payloads and dispatches them to the configured provider. Most providers route through Apprise, which handles delivery for Pushover, Telegram, Slack, Email, Gotify, Matrix, and custom Apprise-compatible URLs. Discord is the main exception for standard webhook URLs, which ChannelWatch sends directly over HTTP.

The current encryption-at-rest scope is intentionally narrow: per-DVR api_key values can be stored with Fernet encryption using /config/encryption.key. That key is created with 0600 permissions when needed. Other secrets are still masked in the UI and in sanitized debug output, but should not all be described as encrypted at rest.

All persistent settings live in /config/settings.json. The Configuration System:

  • Loads settings on startup and watches for changes (hot reload in v0.9.10)
  • Validates settings against a typed schema before applying them
  • Writes a backup to /config/backups/ before any migration step
  • Exposes a read/write API to the Web UI
  • Accepts a subset of settings via environment variables for automation (env vars take precedence over settings.json for the fields they cover)

The SQLite database at /config/channelwatch.db stores activity history (stream events, notification delivery records) separately from configuration. This separation means you can reset configuration without losing history, and vice versa.

The Extension Framework is the plugin layer that makes it possible to add new notification providers without modifying the core codebase. Plugins are Python modules placed in /config/plugins/notifications/ and discovered automatically on startup.

A plugin can register:

  • A new notification provider (subclass of the base provider class) to deliver to a new service

A typical channel-watching notification follows this path:

  1. Channels DVR emits a 1-file-* SSE event when a stream starts
  2. Core Backend parses the event and extracts channel, program, device, and stream metadata
  3. Alert System checks whether Channel Watching alerts are enabled for that DVR, deduplicates against active sessions, and builds an alert payload
  4. Notification System looks up the configured provider for that DVR, encrypts nothing (credentials are already decrypted in memory), and dispatches the payload
  5. The configured delivery path returns success, and the Notification System writes a delivery record to SQLite
  6. The Web UI's Delivery Log reflects the new record on the next poll

Single-container model. ChannelWatch runs as one container with supervisor managing the Python backend and the Node.js UI server. This keeps deployment simple for home lab users. Multi-replica HA is on the roadmap but is not part of v0.9.10.

SQLite over a network database. Activity history and delivery logs are stored in SQLite rather than PostgreSQL or Redis. For a single-container home lab tool, SQLite is faster to set up, requires no external service, and is reliable enough for the write volume ChannelWatch generates.

Mixed async runtime. The v0.9.10 runtime uses asyncio for orchestration and per-DVR task groups, while some monitor work still uses internal threads. Treat it as a single-container, multi-DVR runtime, not a fully thread-free implementation.

Selective encryption at rest. The current code encrypts per-DVR API keys with the auto-generated key at /config/encryption.key. That reduces accidental plaintext exposure for those DVR credentials, but it is not a blanket claim that every provider secret or shared credential in settings.json is encrypted.