Files
LumaOps-Public/docs/ARCHITECTURE.md
T

11 KiB

LumaOps architecture

System shape

LumaOps is a single-container, local-first control plane around the imported OpenRGB 1.0rc3 hardware engine.

Browser
  │ HTTP / SSE on WEB_PORT (bridge default 1223)
  ▼
FastAPI application ───── SQLite + encrypted connector secrets
  │
  ├── command service ─── audit, idempotency, rate limits, locks
  │       │
  │       └── connector registry
  │              ├── OpenRGB connector ── SDK v5 TCP ── OpenRGB Core
  │              ├── WLED connector       HTTP/UDP       (optional)
  │              ├── Home Assistant       REST/WS        (optional)
  │              └── future connectors
  │
  └── built React application

Container supervisor
  ├── OpenRGB: 127.0.0.1:6742 only
  └── FastAPI: APP_HOST:APP_PORT

Only the internal APP_PORT is mapped to WEB_PORT; the defaults are 1223:8080. The OpenRGB SDK has no authentication or encryption and is never a public container port.

Repository boundaries

  • The imported OpenRGB tree remains at the repository root.
  • lumaops/backend contains the FastAPI package and Python tests.
  • lumaops/frontend contains the React/TypeScript application.
  • lumaops/backend/src/lumaops_backend/migrations contains ordered SQLite migrations.
  • docker contains entrypoint/supervision, health, and Unraid assets.
  • docs contains LumaOps operational and design documentation.
  • A minimal OpenRGB core patch is isolated and documented separately.

Runtime lifecycle

  1. Tini becomes PID 1 and forwards container signals.
  2. The LumaOps supervisor creates persistent directories and validates their permissions.
  3. It starts OpenRGB with an explicit loopback address and persistent config path, then captures OpenRGB output in /logs/openrgb.log.
  4. It starts FastAPI regardless of whether hardware detection succeeds.
  5. FastAPI runs migrations, creates an application instance ID/keyfile if needed, and starts the connector registry.
  6. The OpenRGB connector negotiates SDK protocol 5 and synchronizes inventory.
  7. A device-list event or reconnect schedules another read-only inventory sync.
  8. On SIGTERM/SIGINT, FastAPI stops accepting work, connector tasks are cancelled, OpenRGB receives SIGTERM, and the supervisor waits before a bounded SIGKILL fallback.

An OpenRGB failure degrades hardware functions but does not take down the web application. The supervisor restarts OpenRGB with bounded backoff. Repeated failure opens a circuit and surfaces a recovery action in Diagnostics.

Backend modules

The application follows ports-and-adapters boundaries rather than exposing SDK types through the API:

  • connectors.base: normalized capabilities, inventory, state, health, errors, and connector contract.
  • connectors.openrgb.protocol: bounded SDK-v5 parser and serializer.
  • connectors.openrgb.adapter: connection, handshake, request correlation, reconnect, profile operations, and change events.
  • connectors.mock: deterministic test/development implementation that can be enabled only by an explicit non-production setting.
  • services.inventory: stable identities and reconciliation.
  • services.commands: validation, emergency stop, idempotency, locks, rate limiting, execution records, and audit.
  • services.scenes: capture/apply/preview and compensating rollback.
  • services.automations: schedule evaluation, cooldown, conflict keys, and run history.
  • services.setup: read-only environment and hardware checks.
  • services.diagnostics: redacted support bundle generation.
  • api: versioned resource routers under /api/v1, plus SSE.

Connector contract

Every connector exposes:

  • lifecycle: start, stop, test, and health;
  • discovery and inventory without implicit writes;
  • normalized capabilities and current state;
  • validated set_state operations;
  • optional profile/preset operations;
  • timeouts, bounded retries, rate limits, and normalized error categories;
  • a configuration schema whose secret fields are explicitly marked;
  • an inventory-change event callback.

The UI operates only on normalized devices and capability flags. It never contains an OpenRGB packet, WLED segment, or Home Assistant service-call path.

Device identity and ownership

An inventory record has an immutable internal UUID. A connector provides a candidate fingerprint from the most durable available fields, in priority order: connector ID, hardware serial, stable location/path, MAC/IP plus model, then a guarded composite of vendor/model/name. The device_identities table persists all observed aliases so a controller moving to another discovery index keeps its UUID.

controller_index is volatile routing data and is updated during each OpenRGB sync. Before every write, the adapter compares the stored fingerprint with the current controller at that index. Mismatch cancels the command and triggers a resync.

One physical light has one management owner: OpenRGB, native connector, Home Assistant, or unmanaged. Possible duplicates are surfaced for administrator resolution and are never written through two connectors concurrently.

Normalized capability model

Capabilities are explicit booleans plus ranges:

  • power and restore;
  • RGB, colour temperature, and brightness;
  • effect/mode, speed, direction, and multiple colours;
  • per-zone, per-segment, and per-LED control;
  • presets/profiles;
  • read-only or experimental state.

Commands are intersected with capabilities. Unsupported properties result in a 422 response before any connector is called. Brightness, speed, indices, colours, list lengths, and update frequency are range-checked twice: at the API boundary and in the connector.

Command path and safety

Every mutation follows this path:

API validation
  → authentication/CSRF policy
  → idempotency lookup
  → emergency-stop/read-only/block policy
  → stable identity resolution
  → capability validation
  → connector + device locks
  → token-bucket rate limit
  → connector timeout/circuit breaker
  → result + audit record + realtime event

Commands have UUIDs and record request ID, actor, target, sanitized desired state, start/end time, duration, result, and normalized error. Secret values and raw environment data are never command payloads.

The emergency stop cancels queued effects, prevents new hardware writes, and sends one bounded all-off attempt only when the administrator requests it. It does not loop on failing hardware.

Scenes and rollback

LumaOps scenes are application-level, versioned desired-state documents. They can contain OpenRGB devices, connector devices, groups, and explicit off entries. OpenRGB profiles remain separate objects.

Scene application first resolves targets and snapshots retrievable prior state. Commands then execute in deterministic connector/device order. If a required item fails, already changed devices receive compensating commands where their prior state is known. The result reports applied, skipped, failed, and rolled back targets; rollback is best effort and never hides the original failure.

Automation model

Rules store a trigger document, action list, timezone, enabled state, cooldown, and conflict key. MVP trigger types are time, weekday, manual, webhook, and device online/offline. Sunrise/sunset becomes active once coordinates are configured. Temperature and Home Assistant events use the same trigger interface when a provider exists.

A single scheduler claims due rules transactionally. The conflict key prevents overlapping runs for the same scene/group/device. All attempts create an automation_runs record, including skipped cooldown/conflict decisions.

Data and migrations

SQLite uses foreign keys, WAL mode, a busy timeout, UTC timestamps, and explicit transactions. Ordered SQL migrations are recorded in schema_migrations. Before a migration marked destructive, the runner creates a timestamped online backup in /data/backups and aborts if that backup fails.

Core tables cover settings, connectors, encrypted secrets, devices, identity aliases, rooms, groups, membership/tags, scenes/items, automations/runs, commands/results, audit events, health samples, discovery runs, ignored devices, idempotency keys, and setup state.

Soft deletion is used for user-managed resources and history-bearing inventory. High-volume health and activity history has a configurable retention policy.

API and realtime updates

REST resources live below /api/v1. Responses carry X-Request-ID; failures use one stable shape:

{
  "error": {
    "code": "device_offline",
    "message": "Het apparaat is niet bereikbaar.",
    "request_id": "...",
    "details": {},
    "recovery": ["Controleer de connector", "Voer een nieuwe scan uit"]
  }
}

List endpoints use limit, offset, filtering, and documented sort fields. Writes that could be retried accept Idempotency-Key. Server-Sent Events at /api/v1/events distribute health, inventory, command, and audit changes.

FastAPI also serves the compiled SPA and its hashed assets. API and health paths are excluded from SPA fallback.

Configuration and secrets

Non-secret settings come from environment variables and the settings table; environment values win for deployment-critical addresses and paths. Connector secrets are encrypted with Fernet-compatible authenticated encryption using a persistent key from LUMAOPS_SECRET_KEY or /config/lumaops/secret.key.

The keyfile is created with mode 0600 when absent. Losing it makes connector secrets unrecoverable but does not corrupt inventory, scenes, or audit data. Rotation decrypts all values in one transaction using the old key and reencrypts them with the new key after a backup.

Health model

/health/live reports only whether the web process can serve requests. /health/ready verifies database access and migrations. /api/v1/health aggregates the supervisor heartbeat, OpenRGB process, SDK protocol/connection, database, connectors, and persistent-directory writability.

Aggregate states are healthy, degraded, and unhealthy. Missing hardware or SDK is degraded, not a web-process crash. The container healthcheck treats a working but degraded application as alive while exposing the degradation in the detailed API.

Deployment invariants

  • x86_64 Linux and one production image.
  • No compiler, source tree, npm cache, or build tool in the runtime stage.
  • /config, /data, and /logs are persistent and writable.
  • No default privileged: true; only explicit device mappings.
  • Bridge networking by default; documented host mode for LAN discovery.
  • No SaaS dependency and no automatic reverse-proxy or external exposure.
  • Mock mode is rejected when LUMAOPS_ENV=production and is visibly marked in every page when explicitly enabled elsewhere.