# Proposal: Runtime Service Registry

**Change:** `service-registry`
**Phase:** proposal
**Date:** 2026-06-19
**Status:** awaiting review (design only — no implementation yet)

## Context and problem

Phase 1 shipped a configurable dashboard widget system whose service URLs (Grafana,
Prometheus) and app credentials (Jellyfin, Jellyseerr) are driven by environment
variables and machine-level fields. This has three problems:

1. **Operators cannot change services without a redeploy.** Adding a second Grafana,
   pointing Prometheus at a different host, or rotating a Jellyfin API key requires
   editing env vars and restarting containers.
2. **Configuration is split across three places.** Service URLs live in env vars
   (`GRAFANA_URL`, `PROMETHEUS_URL`), Jellyfin/Jellyseerr live on machine records, and
   widget instances live in the widget table. There is no single "what is configured"
   view.
3. **The widget registry is decoupled from the services it depends on.** A Grafana
   widget does not know which Grafana instance it talks to; the widget config holds a
   `dashboard_uid` while the base URL is global.

## Proposal

Introduce a **runtime service registry** persisted in the backend SQLite database:

- Each **service instance** (e.g. "Production Grafana", "Home Jellyfin") is a DB record
  carrying its non-secret config and encrypted secret fields.
- **Service definitions** live as Python modules with Pydantic classes in the repo. Each
  definition declares its config schema, its secret fields, and the **widget kinds** it
  provides (with their own config schemas).
- **Service pages** at `/services/:serviceType/:serviceId` render the service-specific UI
  and list the widgets that service can contribute to the dashboard. These replace the
  existing addon pages.
- **Dashboard widgets** become service-bound: a widget instance references a `service_id`
  and a `widget_kind` drawn from that service's definition.
- Machine records are reduced to **transport only** (SSH + node_exporter); the
  machine-level Jellyfin/Jellyseerr app fields are removed.

## Goals

- One source of truth for every external service the app talks to.
- Add/reconfigure/rotate a service from the UI with no redeploy.
- Multiple instances per service type (two Grafanas, two Jellyfins).
- Centralized, version-controlled service definitions that are easy to extend.
- Widgets discoverable per-service and individually addable to the dashboard.
- Secrets (API keys / tokens) encrypted at rest.

## Non-goals

- **No general-purpose plugin/marketplace system.** Service definitions are closed,
  compile-time code. Adding a brand-new service still requires a backend deploy and a
  Python module.
- **No OAuth token exchange per service in this change.** Only API keys / tokens are
  stored (encrypted). OAuth-proxy flows (e.g. Grafana behind Authentik) continue to be
  handled externally.
- **No drag-and-drop dashboard layout, no grid, no per-user dashboards.** This change
  keeps the existing single stacked-column dashboard model.
- **No in-app charting.** The thin-dashboard observability rule still holds; service
  pages surface deep-links and metadata only.
- **No silent data migration.** Machine-level Jellyfin/Jellyseerr config is removed
  without an automatic converter (see Decisions).

## Decisions (from grilling)

| Topic | Decision |
|-------|----------|
| Scope of services | All current services: Grafana, Prometheus, Jellyfin, Nextcloud, and the SSH task runner. Definitions centralized in repo. |
| Definition format | Python modules with Pydantic classes for service config and widget config, combined under each service definition. |
| Auth storage | API keys / tokens only, encrypted at rest. |
| Encryption key | Single env-provided master key (`MANAGE_ENCRYPTION_KEY`). |
| Machine app config | Services **replace** machine-level Jellyfin/Jellyseerr app config. Machines become SSH/monitoring transport only. |
| Migration | **Break backwards compatibility.** Users re-enter service config after upgrade; no automatic converter. |
| Multi-instance | Yes — multiple service records per service type. |
| Addon pages | Replaced by generic service pages at `/services/:serviceType/:serviceId`. |
| Widget binding | The service definition **owns** its widget config schemas. Widgets are instantiated from a service instance + a widget kind. |

## Risks

- **Breaking upgrade.** Existing deployments lose their Jellyfin config and must re-enter
  it. We must document this loudly in the changelog and README.
- **Encryption key management.** Losing `MANAGE_ENCRYPTION_KEY` makes all stored secrets
  unrecoverable. Key rotation requires re-encrypting every service record.
- **Large surface area.** This change touches backend models, settings store, widget
  registry, adapters, frontend routing, dashboard config UI, and docs. It must be split
  into reviewable PRs (see `tasks.md`).
- **SSH task runner as a service** needs care: saved tasks already have their own
  registry. The service record should hold connection/auth; the task registry stays.
- **Env vars are not fully eliminated.** The encryption key and core auth/OIDC settings
  still require env vars; only service URLs/credentials move to the DB.

## Out of scope for this proposal

- Automatic migration tooling from machine app config to service records.
- Secret rotation UI or key-rotation workflow.
- Per-user or multi-dashboard support.
- Runtime/hot-reload of service definition files (definitions are loaded at startup).
