Feature Specification: Components

Created: 2026-10-07

Status: Implemented

Input: Reconstructed from the data model, the component helpers, the component sets and the group setups of core at 093e68b6f, pull request #1383 and issue #527.

Index

hosting-provider

shared

operator

dev

User Scenarios & Testing (mandatory)

This family spec describes what all components share. The fields of a component, the fold of components into a group, and the evaluation failure for a missing provider are in Data model and extension seam. Groups and wiring are in Setups and wiring. The operator-facing subset is in Applications catalog. Each component has its own spec, listed in the Index.

User Story 1 - Add a component to a group (Priority: P1)

A contributor adds a service to a group. The contributor writes a component module in the directory of the group, and adds it to the component set of the group.

Why this priority: a group can deploy only the components that its set imports.

Independent Test: a unit test of the data model, and the catalog check.

Acceptance Scenarios:

  1. Given a component in the set of the hosting-provider group, When the group setup evaluates, Then the component is in the components of the group.
  2. Given a component with no package and no description, When its description is read, Then the evaluation fails with an error that says that the component must state its own description.
  3. Given a configurable component, When the model evaluates, Then the component is an application.

User Story 2 - State what a component needs and provides (Priority: P1)

A component states what it needs as resource requests in its implementation. It states what it supplies to the group as resource names that it provides. The setup routes each request to a provider in the same deployment.

Why this priority: wiring by capability is principle V of the constitution. No component names the node that supplies its needs.

Independent Test: a unit test of the resource routing.

Acceptance Scenarios:

  1. Given an implementation that requests TLS certificates for its domain in a request group tls, When the setup reads the requests, Then the ssl resource policy receives the request of that group.
  2. Given a component that provides oidc, When the routing folds the group, Then the deployment provides oidc with no edit to the routing.
  3. Given a node that needs a resource that no component of its deployment provides, When the setup evaluates, Then the evaluation fails.
  4. Given a roster that runs the authelia component on a node named auth-01, When the routing looks up the provider of oidc, Then it gives the address of auth-01. A node named authelia that runs another component gives no address.

User Story 3 - Offer upstream NixOS options to the operator (Priority: P2)

An application wraps a stock NixOS module. It builds its module with the NixOS service helper, and selects which upstream options the operator sees.

Why this priority: the operator sees the type, default and description of upstream, and the application writes no copy of them.

Independent Test: unit tests of the NixOS service helper and of the option selection.

Acceptance Scenarios:

  1. Given an application that exposes the upload size limit of the upstream pixelfed service, When the operator form renders, Then it shows that option with the upstream type and default.
  2. Given the operator sets an exposed option, When the application module evaluates, Then the value reaches the upstream service.
  3. Given a selection that names an option that nixpkgs does not have, When the module evaluates, Then the evaluation fails.

User Story 4 - State the store and certificate needs (Priority: P3)

A component states whether it can share the store of the hypervisor (its store mode), and who reaches its served names (its certificate audience). The setup derives the store mode of the node and the certificate authority of each name.

Why this priority: the answer follows from the component, not from the node that runs it.

Independent Test: unit tests of the shared store and of the TLS routing.

Acceptance Scenarios:

  1. Given a component with the store mode own, When the setup folds the node, Then the node does not boot off the store of the host.
  2. Given a component with the audience bootstrap, When the TLS routing routes its names, Then they stay on the private CA.
  3. Given a component with the audience browser, When the routing runs, Then its names move to the public CA.

Edge Cases

  • A component with a default subdomain gets a hidden domain, derived as that subdomain under the domain of the deployment. The operator cannot set the domain directly.
  • A component with no default subdomain gets a visible domain with no default. The deployment states a fully-qualified name, for a name in a zone that the deployment does not own.
  • A component that serves both bootstrap and browser names declares bootstrap. The TLS routing claims a UI domain that only browsers reach back for the public CA.
  • The garage resource answers requests only through a deploy-time graph, so no component provides it. The garage component provides the objectStore backend.
  • An application module that sets an option at full priority, which the operator also sets through the NixOS service helper, fails evaluation with a conflict.
  • git-pages declares no component. It gives a NixOS module that the dev group imports into the host configuration of the forge machine, and TLS names that the group adds to the extra domains of the forgejo component.

Requirements (mandatory)

Functional Requirements

  • FR-001: Each component MUST be a module in the directory of its group that declares one component.
  • FR-002: Each group MUST have one component set that lists its components. The group setup MUST name that set.
  • FR-003: The hosting-provider set MUST include terraform-backend, netbox, incus, openbao, overview, garage, postgresql, bind, windmill, otel-collector, telemetry, spire and the four shared components.
  • FR-004: The operator set MUST include grafana, mastodon, peertube, pixelfed and the four shared components.
  • FR-005: The dev set MUST include forgejo and forgejo-runner.
  • FR-006: The shared components valkey, smtp, lldap and authelia MUST be in one shared set, and each group that imports them MUST run one instance of each.
  • FR-007: A component set MUST NOT import a module from api or panel. A consumer MUST add front-end components through the groups seam.
  • FR-008: The implementation of a component MUST map its settings to named request groups. Each request group MUST hold one request of a resource of core.
  • FR-009: The resources of a component MUST default to its config mapping, applied to its settings.
  • FR-010: The module of a component MUST carry the static modules that mark it as a component configuration and that derive its domain. The default module MUST declare only the switch that turns the component on.
  • FR-011: The shared options helper MUST give the switch that turns the component on, a subdomain when the component has a default subdomain, and a domain (hidden when a subdomain exists, else visible with no default).
  • FR-012: The NixOS service helper MUST take the upstream options, the selection of upstream options to expose, the location of the upstream service, the own options of the component and the narrowed options, and MUST return the module of the component.
  • FR-013: The NixOS service helper MUST let own options shadow upstream options at the same path, and MUST NOT forward own options to the upstream service.
  • FR-014: The NixOS service helper MUST forward to the upstream service only the options that the operator set.
  • FR-015: Narrowing an option that the selection does not expose MUST fail evaluation.
  • FR-016: A component MUST provide only resources of core, or the backends varsBackend (the store of the generateFiles contract), pki (the CA of the openbao-pki provider of ssl), otelBackend (the stores that the otel provider exports to), instances (the hypervisor of the group) and objectStore (the S3 store of the group). The resource routing MUST fold what the components provide per deployment.
  • FR-017: The store mode MUST be one of shared (the default), host-daemon or own. The setups MUST fold it into the store mode of each node: whether the node shares the store of the host, and whether it uses the Nix daemon of the host.
  • FR-018: The certificate audience MUST be one of challengeable (the default), bootstrap or browser. The TLS routing MUST read it.
  • FR-019: An infrastructure component MUST state the label of its UI as its UI subdomain. An operator-facing component MUST state its label as a default subdomain instead.
  • FR-020: core MUST export the helpers windmill, windmill-runner, operatorCuration, deployCredentials, registrarTransfer, machine and contractModules for a front end that runs the hosting-provider group. Each helper MUST keep the contract in Helpers for a front end. A helper MUST NOT take an input that only one front end can supply.
  • FR-021: A wiring decision that needs the node of a provider MUST find it through the provider lookup of the resource routing: the roster node whose component provides the resource, or none when no roster node does. The evaluation MUST fail when more than one roster node provides the resource.

Helpers for a front end

The caller is a front end that runs the hosting-provider group. It adds its own nodes to the group through the groups seam (FR-007) and reads these helpers from the library of core. The names of the inputs and of the result fields are part of the contract.

  • windmill: a package, with no input. It is Windmill with only the job languages of the deploy scripts. It does not configure or start a Windmill service, and it does not upload scripts.
  • windmill-runner: the Python module that runs each nix subprocess of the deploy scripts. The caller puts it into its own Python package to run the same subprocesses. It does not start a process.
  • operatorCuration: a function.
    • Inputs: the results of the netbox and dns contracts for the node (netbox, dns; required), and four operator lanes, each absent by default: the DNS provider of the operator (operatorDns), the registrar of the operator with its nameservers, cost limit, owner handle and optional zones (operatorRegistrar), the NetBox range selector (operatorNetbox), and the uplink of the operator (operatorUplink).
    • Result: the effect-defaults half of the hosting configuration, for the operator hosts and the operator zone. The operator hosts get a domain registration only when the registrar lane is set, the DNS lane is absent and the dns provider is bind. The domain registration has zones only when the registrar lane names zones.
    • It does not read the environment or a module configuration, it carries no credential or token file, and it does not render JSON.
  • deployCredentials: a function.
    • Inputs: the nixpkgs library (lib), the secrets mount and prefix of the node (mount, prefix), and the result of operatorCuration or none (hostingConfig; required, with no default).
    • Result: reads, the vars keys, as <instance>/<key>, that the deploy runners can read; zonesManagerReads, the vars key of the zones-manager token, which only the front-end node can read; operatorLane, the DNS write lane of the curation, or none; registrar, the name of the registrar of the domain registration, or none when the curation registers no domain; registrarLogin, the login of that registrar account, a subset of the secrets; secrets, a map from each variable name to its path in the secrets store of the node, with a mark on a value that can be absent, in the request shape of the envSecrets contract; variables, the secrets plus the admin sources, which are deployment data and not a credential; and zonesManager, the path of the zones-manager token only when the domain registration has zones. The DNS token and the registrar login follow the curation. The zones-manager token goes to the front-end node only, and is in neither the secrets nor the variables.
    • The evaluation fails when the hosting configuration is not an attribute set or none, or when a path is not on a granted instance.
    • It does not read a value, request a secret or grant a read.
  • registrarTransfer: an attribute set from the registrar of deployCredentials to a package, with no input. A registrar that is not in the set has no command. The command of each package reads and changes the transfer state of a domain of the registrar account, for a migration of a deployment to a different provider. Each command has the same interface, thus the caller does not need to know which registrar the account is at.
    • holds <domain> gives {"held": <bool>}: whether the account holds the domain.
    • release <domain> unlocks the domain and gives {"auth_code": "<code>"}, or {"auth_code": null} when the account does not hold the domain.
    • retake <domain> resets the auth code of the domain and locks it, and gives {"held": <bool>}. It changes nothing when the account does not hold the domain.
    • It reads the login of the account from the environment, as the operator deploys get it (registrarLogin). The openprovider command also reads OPENPROVIDER_BASE_URL.
    • When the registrar fails, it stops with exit code 1 and writes the cause on stderr, without a credential or an auth code.
  • machine: a function of a node name.
    • Result: a published roster entry with that name. The caller adds it to the published machines of the hosting-provider group through the fold of group extensions.
    • The evaluation fails when the name is not a string.
    • It does not declare a component and it does not allocate an address.
  • contractModules: the NixOS modules that declare the netbox and dns contracts. A module of the caller imports them to read the results that operatorCuration takes. They do not request a resource or set a result.

Key Entities

  • Component: an entry of the components of the model, with the fields in Data model and extension seam plus the store mode and the certificate audience.
  • Component set: the list of the components of one group.
  • Request group: a named entry of the result of an implementation, for example tls, spiffe, code or incus-oidc, that holds one resource request.
  • Capability: a resource name. A component needs it by a request and provides it by name.
  • Shared component: a component of the shared set, which more than one group imports.
  • Shared options helper: a helper function, not a module, for the switch, the subdomain and the domain of a component.
  • NixOS service helper: a helper function that wraps a stock NixOS option set into the module of a component.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: The unit tests of the data model and the catalog check pass.
  • SC-002: The unit tests of the resource routing pass, including the evaluation failure for a resource that no deployment provides.
  • SC-003: The unit tests of the NixOS service helper and of the option selection pass.
  • SC-004: The unit tests of the shared store and of the TLS routing pass.
  • SC-005: The unit tests of the helpers for a front end and of the operator curation pass.

Assumptions

  • Each group setup imports exactly one component set. The hypervisor group reuses the hosting-provider set.
  • A component maps to one node. A second service on the same node is a NixOS module in the host configuration of that node, as git-pages is on the forge node.
  • The resource specs in Resources describe each request type.
  • A mapping of the data model holds a type-checked function, and is named for the input of that function. The config mapping of a component takes the component settings and gives its requests. The resource mapping of an environment takes the required resources, and the config mapping of an environment takes the operator configuration. Both give a deployment.
  • A caller calls each mapping through its apply. The apply of a resource policy has the same name because a caller calls it the same way: the call checks the argument and gives the result.

Known Deviations

  • The request groups of a component are keyed by name, not by resource type. A TODO asks to transpose them and group them by type.