Feature Specification: Service Deployment

Created: 2026-09-23

Status: Implemented

Input: Reconstructed from the code at 0bf4ecd36 (panel/default.nix, panel/flake.nix, panel/nix/configuration.nix, panel/nix/package.nix, panel/nix/component/, panel/nix/tests.nix, panel/nix/docs.nix, panel/nix/screenshots.nix, panel/nix/db-diagram.nix, panel/checks/, panel/src/panel/settings.py, panel/src/panel/management/, panel/docs/), pull requests #1105, #1182, #1650, #1659, #1714, #1730, #1766, #1815, #1893, #1901 and #1902, and the design notes of the maintainers.

User Scenarios & Testing (mandatory)

User Story 1 - Run the panel as a NixOS service (Priority: P1)

A provider enables services.panel, sets the domain, the address of api, the secrets and optionally the identity provider and the WebSocket host of the Centrifugo of api. The module runs the panel with uvicorn behind nginx, collects the static files, runs the database migrations, and gives the manage command to the administrator.

Why this priority: this is how a provider runs the panel.

Independent Test: run the check panel-basic. The panel answers on its domain, and an administrator can log in.

Acceptance Scenarios:

  1. Given services.panel.enable and a domain, When the machine starts, Then the panel answers on the domain through nginx.
  2. Given production = true, When the machine starts, Then nginx redirects HTTP to HTTPS and serves the panel over TLS, with the certificate that the SSL resource of the component writes.
  3. Given a changed OIDC client secret file, When the file changes, Then the panel restarts and reads the new secret.
  4. Given a database URL, When the panel starts, Then the panel uses that database. Without it, the panel uses SQLite in its state directory.

User Story 2 - Configure the panel with the environment (Priority: P1)

The panel reads its settings from the environment: SECRET_KEY (a credential), API_HTTP_URL, API_BROWSER_BASE_URL, CENTRIFUGO_BROWSER_WS_URL, OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET_FILE, OIDC_SCOPES, OIDC_DISPLAY_NAME, OIDC_REDIRECT_IS_HTTPS, SESSION_COOKIE_DOMAIN, API_TOKEN_TTL, RECONCILE_TOKEN_TTL, DATABASE_URL and USER_SETTINGS_FILE. The module writes the Django settings of services.panel.settings to the user settings file.

Independent Test: run panel-python-tests (test_runtime_settings.py).

Acceptance Scenarios:

  1. Given a user settings file, When the panel starts, Then its values replace the defaults.
  2. Given that SECRET_KEY is missing, When the panel starts, Then the panel stops with an error.

User Story 3 - Develop and check the panel (Priority: P2)

A developer enters the development shell, runs the panel against a local api, and runs the checks. The checks cover the Python tests, the JavaScript tests, the migrations, the documentation, the database diagram, a basic VM test and an end-to-end VM test with api.

Independent Test: nix build ./panel#checks.x86_64-linux.treefmt, and each other check in checks.

Acceptance Scenarios:

  1. Given a model change without a migration, When panel-migrations runs, Then the check fails.
  2. Given the documentation, When panel-docs builds, Then it includes the options of the module, the user guide for operators and the screenshots.

User Story 4 - Choose a light or dark theme (Priority: P3)

The operator switches between the light and the dark theme with one control. The page follows the preference of the system until the operator chooses.

Independent Test: run panel-python-tests (test_theme_toggle.py).

Acceptance Scenarios:

  1. Given a first visit, When the page opens, Then it follows the preference of the system.
  2. Given a choice of the operator, When the operator opens another page, Then the choice stays.

Edge Cases

  • The identity provider uses a private CA: the component gives the CA bundle to the panel.
  • Centrifugo is on a different subdomain: the session cookie domain must include both hosts. When the subdomain of Centrifugo is below the domain of the panel, the cookie domain is the domain of the panel, so other hosts of the parent domain do not get the cookie.
  • API_BROWSER_BASE_URL is empty: the browser calls api on the host of the panel, as the description of the option says.

Requirements (mandatory)

Functional Requirements

  • FR-001: panel/default.nix MUST give all outputs. panel/flake.nix MUST only add the ${system} layer.
  • FR-002: The NixOS module MUST take the address of api, the identity provider, the event service and the secrets as configuration.
  • FR-003: The panel MUST read its secrets from files or systemd credentials, never from the Nix store.
  • FR-004: The panel MUST run the database migrations before it serves requests.
  • FR-005: The documentation MUST describe the current options of the module.
  • FR-006: The documentation MUST include a user guide that tells an operator how to deploy, use, change and remove a deployment.
  • FR-007: The screenshots of the documentation MUST come from the UI that the revision builds. The capture (nix/tests.nix) MUST seed api only through its HTTP API. A fake Windmill behind api (nix/fake-windmill.py) reports each deploy as completed with success, so the pages show a deployed state while nothing is built.

Key Entities

  • NixOS module: services.panel.* in nix/configuration.nix.
  • Component: the wiring of the panel into a group of the hosting provider, in nix/component/.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: A provider can run the panel with the module and its own identity provider, api and event service, without the component.
  • SC-002: All checks of panel pass on each pull request that changes panel/.

Assumptions

  • api and Centrifugo run as separate services that the module can reach by address.

Known Deviations

None.