Feature Specification: Components

Created: 2026-10-07

Status: Implemented

Input: Reconstructed from the code at 093e68b6f (core/lib/data-model.nix, core/components/sharedOptions.nix, core/components/nixosService.nix, core/components/hosting-provider/default.nix, core/components/operator/default.nix, core/components/dev/default.nix, core/components/dev/git-pages/default.nix, core/setups/groups/*.nix, core/default.nix), pull request #1383 and issue #527.

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 core/components/<group>/<name>/default.nix, which sets components.<name>. The contributor adds the directory to the imports of core/components/<group>/default.nix.

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

Independent Test: nix-unit-lib-data-model and applications-catalog.

Acceptance Scenarios:

  1. Given a component directory in the imports of core/components/hosting-provider/default.nix, When core/setups/groups/hosting-provider.nix evaluates, Then the component is in components of the group.
  2. Given a component with no package and no description, When its description is read, Then evaluation fails with "names no package, so it must state its own description".
  3. Given a component with configurable = true, When the model evaluates, Then the component is in applications.

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

A component states what it needs as resource requests in implementation. It states what it supplies to the group as resource names in 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: nix-unit-setups-resource-routing.

Acceptance Scenarios:

  1. Given implementation returns { "tls".ssl.domains = [ domain ]; }, When the setup reads the requests, Then the ssl resource policy receives the request of group tls.
  2. Given a component with provides = [ "oidc" ], When core/setups/wiring/resource-routing.nix folds the group, Then the deployment provides oidc with no edit to the routing file.
  3. Given a node that needs a resource no component of its deployment provides, When the setup evaluates, Then evaluation fails.
  4. Given a roster that runs the authelia component on a node named auth-01, When hostOf looks up 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 nixosService.nix 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: nix-unit-lib-nixos-service-options and nix-unit-lib-option-selection.

Acceptance Scenarios:

  1. Given expose = [ "maxUploadSize" ] and optionPath = [ "services" "pixelfed" ], When the operator form renders, Then it shows maxUploadSize with the upstream type and default.
  2. Given the operator sets an exposed option, When the application module evaluates, Then the value reaches services.pixelfed through nixosServiceConfig.
  3. Given a selection that names an option nixpkgs does not have, When the module evaluates, Then 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 and who reaches its served names. 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: nix-unit-setups-shared-store and nix-unit-setups-tls-routing.

Acceptance Scenarios:

  1. Given a component with nixStore = "own", When core/setups/hosts-common.nix folds the node, Then the node does not boot off the store of the host.
  2. Given a component with certAudience = "bootstrap", When core/setups/wiring/tls-routing.nix routes its names, Then they stay on the private CA.
  3. Given a component with certAudience = "browser", When the routing runs, Then its names move to the public CA.

Edge Cases

  • A component with defaults.subdomain gets a hidden domain that the static componentDomainModule derives as <subdomain>.<deploymentDomain>. The operator cannot set domain directly.
  • A component without defaults.subdomain gets a visible domain with no default. The deployment states a fully-qualified name, for a name in a zone the deployment does not own.
  • A component that serves both bootstrap and browser names declares bootstrap. The TLS routing claims a browser-only uiDomain back for the public CA.
  • The garage resource answers requests only through a deploy-time graph, so it does not appear in provides. The garage component provides the objectStore backend.
  • An application module that sets an option at full priority, which the operator also sets through nixosService.nix, fails evaluation with a conflict.
  • git-pages declares no component. It exports a NixOS module that the dev group imports into the host config of the forge machine, and TLS names that the group adds to the extraDomains of the forgejo component.

Requirements (mandatory)

Functional Requirements

  • FR-001: Each component MUST be a module in core/components/<group>/<name>/default.nix that sets components.<name>.
  • FR-002: Each component set core/components/<group>/default.nix MUST list its components as imports. The group setup in core/setups/groups/ MUST name the set in its components list.
  • FR-003: The hosting-provider set MUST import 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 import mastodon, peertube, pixelfed and the four shared components.
  • FR-005: The dev set MUST import forgejo and forgejo-runner.
  • FR-006: The shared components valkey, smtp, lldap and authelia MUST live in core/components/shared/, 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: implementation MUST map the component settings to component-resources: an attribute set of named request groups, each holding one request typed by a resource of core/resources.
  • FR-009: resources MUST default to the application of config-mapping to the settings.
  • FR-010: module MUST carry the static modules { _class = "fediversity-component-config"; } and componentDomainModule. The default module MUST declare only enable.
  • FR-011: sharedOptions.nix MUST return enable, a subdomain when defaults.subdomain is given, and a domain (hidden when subdomain exists, else with no default).
  • FR-012: nixosService.nix MUST take nixosOptions, expose, optionPath, ownOptions and narrowedOptions, and MUST return a module for components.<name>.module.
  • FR-013: nixosService.nix MUST let own options shadow upstream options at the same path, and MUST NOT forward own options to optionPath.
  • FR-014: nixosService.nix MUST collect only options the operator set, into the hidden option nixosServiceConfig.
  • FR-015: Narrowing an option that expose did not select MUST fail evaluation.
  • FR-016: provides MUST name resources of core/resources, or the backends varsBackend (the store of the generateFiles contract), pki (the CA of the openbao-pki provider of ssl), otelBackend (the stores the otel provider exports to), instances (the hypervisor of the group) and objectStore (the S3 store of the group), and core/setups/wiring/resource-routing.nix MUST fold it per deployment.
  • FR-017: nixStore MUST be one of shared (default), host-daemon or own. core/setups/hosts-common.nix MUST fold it into the sharedStore and hostNixDaemon fields of each node.
  • FR-018: certAudience MUST be one of challengeable (default), bootstrap or browser. core/setups/wiring/tls-routing.nix MUST read it.
  • FR-019: uiSubdomain MUST name the label of the UI of an infrastructure component. An operator-facing component MUST state its label as a subdomain default instead.
  • FR-020: core/default.nix MUST export the helpers windmill, windmill-runner, operatorCuration, deployCredentials, machine and contractModules under lib.components.hosting-provider, 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 with providerNode or hostOf of core/setups/wiring/resource-routing.nix: the roster node whose component provides the resource, or null when no roster node does. 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 core.lib.components.hosting-provider.

  • windmill: a derivation. It takes no input. It is the Windmill package 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: a path to the Python module _runner.py 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 of an attribute set.
    • Inputs: netbox (attribute set, required): the result of core/resources/netbox/contract.nix for the node. dns (attribute set, required): the result of core/resources/dns/contract.nix for the node. operatorDns (null or { provider; serverUrl; }), operatorRegistrar (null or an attribute set with nameservers, maxCost, ownerHandle and the optional zones), operatorNetbox (null or { rangeSelector; }) and operatorUplink (null or { mode; parent; gateway4; gateway6; }), each null by default.
    • Result: { effect-defaults = { tf-incus-operator-hosts; octodns-zone; }; }, the effect-defaults half of HOSTING_CONFIG. tf-incus-operator-hosts has domainRegistration only when operatorRegistrar is set, operatorDns is null and the dns provider is bind. domainRegistration has zones only when operatorRegistrar.zones is set.
    • It does not read the environment or a module config, it carries no credential or token file, and it does not render JSON.
  • deployCredentials: a function of an attribute set.
    • Inputs: lib (the nixpkgs lib), mount and prefix (strings, the services.vars-openbao coordinates of the node), and hostingConfig (the result of operatorCuration, or null; required, with no default).
    • Result: { instances; operatorLane; secrets; variables; zonesManager; }. secrets maps each variable name to a path, plus optional = true for a value that can be absent, in the request shape of contracts.envSecrets, with each path under <mount>/data/<prefix>/secret/. variables is secrets plus ADMIN_SOURCES, which is deployment data and not a credential. instances lists the vars instances that the deploy runners can read. operatorLane is the DNS write lane of the curation, or null. The DNS token and the registrar login follow the curation. zonesManager maps ZONES_MANAGER_TOKEN to its path only when domainRegistration has zones, and is empty otherwise. That token goes to the front-end node only, and is in neither secrets nor variables.
    • Evaluation fails when hostingConfig is not an attribute set or null, or when a path is not on a granted instance.
    • It does not read a value, request a secret or grant a read.
  • machine: a function of a string name.
    • Result: { network.name = name; }, a published roster entry. The caller adds it with foldGroupExtensions { hosting-provider.machines.published.<name> = ...; }.
    • Evaluation fails when name is not a string.
    • It does not declare a component and it does not allocate an address.
  • contractModules: { netbox; dns; }, the paths of the NixOS modules that declare the netbox and dns contracts. A module of the caller imports them to read the results into operatorCuration. They do not request a resource or set a result.

Key Entities

  • Component: an entry of components.<name>, with the fields in Data Model and Extension Seam plus nixStore and certAudience.
  • Component set: core/components/<group>/default.nix, the imports list of one group.
  • Request group: a named entry of the result of implementation, for example "tls", "spiffe", "code" or "incus-oidc", holding one resource request.
  • Capability: a resource name. A component needs it by a request and provides it by provides.
  • Shared component: a component in core/components/shared/ that more than one group imports.
  • sharedOptions.nix: a helper function, not a module, for enable, subdomain and domain.
  • nixosService.nix: a helper function that wraps a stock NixOS option set into a component module.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: nix-unit-lib-data-model and applications-catalog pass.
  • SC-002: nix-unit-setups-resource-routing passes, including the evaluation failure for a resource no deployment provides.
  • SC-003: nix-unit-lib-nixos-service-options and nix-unit-lib-option-selection pass.
  • SC-004: nix-unit-setups-shared-store and nix-unit-setups-tls-routing pass.
  • SC-005: nix-unit-components-hosting-provider-helpers and nix-unit-components-operator-curation pass.

Assumptions

  • Each group setup in core/setups/groups/ 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 config of that node, as git-pages is on the forge node.
  • The resource specs in Resources describe each request type.

Known Deviations

  • config-mapping carries TODO(@fricklerhandwerk): this needs a better name.
  • The apply option of a resource policy in core/lib/data-model.nix carries TODO rename this to be consistent with the component's resource mapping.
  • component-resources carries TODO: maybe transpose, and group the resources by type instead.

Index

hosting-provider

shared

operator

dev