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:
- Given
services.panel.enableand a domain, When the machine starts, Then the panel answers on the domain through nginx. - 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. - Given a changed OIDC client secret file, When the file changes, Then the panel restarts and reads the new secret.
- 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:
- Given a user settings file, When the panel starts, Then its values replace the defaults.
- Given that
SECRET_KEYis 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:
- Given a model change without a migration, When
panel-migrationsruns, Then the check fails. - Given the documentation, When
panel-docsbuilds, 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:
- Given a first visit, When the page opens, Then it follows the preference of the system.
- 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_URLis empty: the browser callsapion the host of the panel, as the description of the option says.
Requirements (mandatory)
Functional Requirements
- FR-001:
panel/default.nixMUST give all outputs.panel/flake.nixMUST 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 seedapionly through its HTTP API. A fake Windmill behindapi(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.*innix/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,
apiand event service, without the component. - SC-002: All checks of
panelpass on each pull request that changespanel/.
Assumptions
apiand Centrifugo run as separate services that the module can reach by address.
Known Deviations
None.