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:
- Given a component directory in the
importsofcore/components/hosting-provider/default.nix, Whencore/setups/groups/hosting-provider.nixevaluates, Then the component is incomponentsof the group. - Given a component with no
packageand nodescription, When itsdescriptionis read, Then evaluation fails with "names nopackage, so it must state its owndescription". - Given a component with
configurable = true, When the model evaluates, Then the component is inapplications.
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:
- Given
implementationreturns{ "tls".ssl.domains = [ domain ]; }, When the setup reads the requests, Then thesslresource policy receives the request of grouptls. - Given a component with
provides = [ "oidc" ], Whencore/setups/wiring/resource-routing.nixfolds the group, Then the deployment providesoidcwith no edit to the routing file. - Given a node that needs a resource no component of its deployment provides, When the setup evaluates, Then evaluation fails.
- Given a roster that runs the
autheliacomponent on a node namedauth-01, WhenhostOflooks upoidc, Then it gives the address ofauth-01. A node namedautheliathat 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:
- Given
expose = [ "maxUploadSize" ]andoptionPath = [ "services" "pixelfed" ], When the operator form renders, Then it showsmaxUploadSizewith the upstream type and default. - Given the operator sets an exposed option, When the application module evaluates, Then the value reaches
services.pixelfedthroughnixosServiceConfig. - 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:
- Given a component with
nixStore = "own", Whencore/setups/hosts-common.nixfolds the node, Then the node does not boot off the store of the host. - Given a component with
certAudience = "bootstrap", Whencore/setups/wiring/tls-routing.nixroutes its names, Then they stay on the private CA. - Given a component with
certAudience = "browser", When the routing runs, Then its names move to the public CA.
Edge Cases
- A component with
defaults.subdomaingets a hiddendomainthat the staticcomponentDomainModulederives as<subdomain>.<deploymentDomain>. The operator cannot setdomaindirectly. - A component without
defaults.subdomaingets a visibledomainwith 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-onlyuiDomainback for the public CA. - The
garageresource answers requests only through a deploy-time graph, so it does not appear inprovides. Thegaragecomponent provides theobjectStorebackend. - An application module that sets an option at full priority, which the operator also sets through
nixosService.nix, fails evaluation with a conflict. git-pagesdeclares no component. It exports a NixOS module that thedevgroup imports into the host config of the forge machine, and TLS names that the group adds to theextraDomainsof theforgejocomponent.
Requirements (mandatory)
Functional Requirements
- FR-001: Each component MUST be a module in
core/components/<group>/<name>/default.nixthat setscomponents.<name>. - FR-002: Each component set
core/components/<group>/default.nixMUST list its components asimports. The group setup incore/setups/groups/MUST name the set in itscomponentslist. - FR-003: The
hosting-providerset MUST importterraform-backend,netbox,incus,openbao,overview,garage,postgresql,bind,windmill,otel-collector,telemetry,spireand the four shared components. - FR-004: The
operatorset MUST importmastodon,peertube,pixelfedand the four shared components. - FR-005: The
devset MUST importforgejoandforgejo-runner. - FR-006: The shared components
valkey,smtp,lldapandautheliaMUST live incore/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/orpanel/. A consumer MUST add front-end components through thegroupsseam. - FR-008:
implementationMUST map the component settings tocomponent-resources: an attribute set of named request groups, each holding one request typed by a resource ofcore/resources. - FR-009:
resourcesMUST default to the application ofconfig-mappingto the settings. - FR-010:
moduleMUST carry the static modules{ _class = "fediversity-component-config"; }andcomponentDomainModule. The defaultmoduleMUST declare onlyenable. - FR-011:
sharedOptions.nixMUST returnenable, asubdomainwhendefaults.subdomainis given, and adomain(hidden whensubdomainexists, else with no default). - FR-012:
nixosService.nixMUST takenixosOptions,expose,optionPath,ownOptionsandnarrowedOptions, and MUST return amoduleforcomponents.<name>.module. - FR-013:
nixosService.nixMUST let own options shadow upstream options at the same path, and MUST NOT forward own options tooptionPath. - FR-014:
nixosService.nixMUST collect only options the operator set, into the hidden optionnixosServiceConfig. - FR-015: Narrowing an option that
exposedid not select MUST fail evaluation. - FR-016:
providesMUST name resources ofcore/resources, or the backendsvarsBackend(the store of thegenerateFilescontract),pki(the CA of theopenbao-pkiprovider ofssl),otelBackend(the stores theotelprovider exports to),instances(the hypervisor of the group) andobjectStore(the S3 store of the group), andcore/setups/wiring/resource-routing.nixMUST fold it per deployment. - FR-017:
nixStoreMUST be one ofshared(default),host-daemonorown.core/setups/hosts-common.nixMUST fold it into thesharedStoreandhostNixDaemonfields of each node. - FR-018:
certAudienceMUST be one ofchallengeable(default),bootstraporbrowser.core/setups/wiring/tls-routing.nixMUST read it. - FR-019:
uiSubdomainMUST name the label of the UI of an infrastructure component. An operator-facing component MUST state its label as asubdomaindefault instead. - FR-020:
core/default.nixMUST export the helperswindmill,windmill-runner,operatorCuration,deployCredentials,machineandcontractModulesunderlib.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
providerNodeorhostOfofcore/setups/wiring/resource-routing.nix: the roster node whose component provides the resource, ornullwhen 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.pythat runs eachnixsubprocess 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): theresultofcore/resources/netbox/contract.nixfor the node.dns(attribute set, required): theresultofcore/resources/dns/contract.nixfor the node.operatorDns(nullor{ provider; serverUrl; }),operatorRegistrar(nullor an attribute set withnameservers,maxCost,ownerHandleand the optionalzones),operatorNetbox(nullor{ rangeSelector; }) andoperatorUplink(nullor{ mode; parent; gateway4; gateway6; }), eachnullby default. - Result:
{ effect-defaults = { tf-incus-operator-hosts; octodns-zone; }; }, theeffect-defaultshalf ofHOSTING_CONFIG.tf-incus-operator-hostshasdomainRegistrationonly whenoperatorRegistraris set,operatorDnsisnulland thednsprovider isbind.domainRegistrationhaszonesonly whenoperatorRegistrar.zonesis set. - It does not read the environment or a module
config, it carries no credential or token file, and it does not render JSON.
- Inputs:
deployCredentials: a function of an attribute set.- Inputs:
lib(the nixpkgslib),mountandprefix(strings, theservices.vars-openbaocoordinates of the node), andhostingConfig(the result ofoperatorCuration, ornull; required, with no default). - Result:
{ instances; operatorLane; secrets; variables; zonesManager; }.secretsmaps each variable name to apath, plusoptional = truefor a value that can be absent, in the request shape ofcontracts.envSecrets, with each path under<mount>/data/<prefix>/secret/.variablesissecretsplusADMIN_SOURCES, which is deployment data and not a credential.instanceslists the vars instances that the deploy runners can read.operatorLaneis the DNS write lane of the curation, ornull. The DNS token and the registrar login follow the curation.zonesManagermapsZONES_MANAGER_TOKENto its path only whendomainRegistrationhaszones, and is empty otherwise. That token goes to the front-end node only, and is in neithersecretsnorvariables. - Evaluation fails when
hostingConfigis not an attribute set ornull, or when a path is not on a granted instance. - It does not read a value, request a secret or grant a read.
- Inputs:
machine: a function of a stringname.- Result:
{ network.name = name; }, a published roster entry. The caller adds it withfoldGroupExtensions { hosting-provider.machines.published.<name> = ...; }. - Evaluation fails when
nameis not a string. - It does not declare a component and it does not allocate an address.
- Result:
contractModules:{ netbox; dns; }, the paths of the NixOS modules that declare thenetboxanddnscontracts. A module of the caller imports them to read the results intooperatorCuration. 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 plusnixStoreandcertAudience. - Component set:
core/components/<group>/default.nix, theimportslist 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, forenable,subdomainanddomain.nixosService.nix: a helper function that wraps a stock NixOS option set into a componentmodule.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001:
nix-unit-lib-data-modelandapplications-catalogpass. - SC-002:
nix-unit-setups-resource-routingpasses, including the evaluation failure for a resource no deployment provides. - SC-003:
nix-unit-lib-nixos-service-optionsandnix-unit-lib-option-selectionpass. - SC-004:
nix-unit-setups-shared-storeandnix-unit-setups-tls-routingpass. - SC-005:
nix-unit-components-hosting-provider-helpersandnix-unit-components-operator-curationpass.
Assumptions
- Each group setup in
core/setups/groups/imports exactly one component set. Thehypervisorgroup reuses thehosting-providerset. - 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-pagesis on the forge node. - The resource specs in Resources describe each request type.
Known Deviations
config-mappingcarriesTODO(@fricklerhandwerk): this needs a better name.- The
applyoption of a resource policy incore/lib/data-model.nixcarriesTODO rename this to be consistent with the component's resource mapping. component-resourcescarriesTODO: maybe transpose, and group the resources by type instead.
Index
hosting-provider
terraform-backendnetboxincusopenbaooverviewgaragepostgresqlbindwindmilltelemetryandotel-collectorspire