Contracts and data flow

This page tells how an operator configuration becomes a deployment, and how contracts connect the software that needs a service to the software that gives it. The data model and hosting resources pages are the option references for the same parts. The concepts page defines the terms.

From a configuration to a deployment

flowchart
  subgraph configuration
    application-config
  end
  subgraph resource[resource module]
    request
    policy
  end
  subgraph application[application module]
    application-options{{application-options}} --> application-config
    application-config --> config-mapping
    request{{request}} --> config-mapping
    config-mapping(config-mapping)
  end
  subgraph fediversity[fediversity setup]
    config-mapping -->|required resources| resource-mapping(resource-mapping)
    policy{{policy}} --> provider-config -->|available resources| resource-mapping -->|configuration| deployment{deployment}
  end
  maintainer((maintainer)) -->|curates| resource
  contributor((developer)) -->|curates| application
  operator((operator)) -->|enters| configuration
  hosting-provider((hosting provider)) -->|maintains| fediversity

In the diagram, a circle is an actor, a box with angled sides is a type, a rectangle is a value, a box with round corners is a function, and a diamond is a state. An arrow points to the part that uses the other part. An application module is a component that the operator can select.

  1. The operator selects the applications and sets their options in the panel. The panel form comes from frontendConfiguration, which has only the operator-facing components. The result is a configuration.
  2. Each enabled component maps its options to the resources that it needs. The implementation of a component, a config-mapping, returns one request for each resource, keyed by an instance name. For example, Forgejo requests a TLS certificate as "tls".ssl, its secrets as "age".secrets, and outbound mail as "forgejo-smtp".smtp.
  3. The hosting provider declares an environment. Its resources set the policy of each resource: which provider gives the resource, and with which limits.
  4. The implementation of the environment, a resource-mapping, collects the requests of all components. For each resource, the process function of the policy turns the requests into NixOS modules or OpenTofu resources.
  5. The result is a deployment for an effect, for example a set of Incus containers that OpenTofu creates and NixOS configures.

environments.<env>.deployment does all of these steps for one deployment.

Resources and contracts

Fediversity uses two levels of interface:

  • A resource is the interface between a component and a hosting provider. The component sets the request, and the hosting provider sets the policy. The request/policy resource pattern on the data model page tells more, and the hosting resources page lists the request options of each resource.
  • A contract is the interface between a consumer module and a provider module of a service, for example smtp, postgresql, ssl or oidc. It follows RFC 189. A resource usually uses one or more contracts to give what a request asks for.
CONTRACT consumer provider request response

For example, Forgejo asks the smtp contract for outbound mail with a sender address. The mail relay that provides the contract answers with a host, a port, a user name and a password file:

SMTP forgejo mail relay { sender } { host, port, ... }

A contract has these parts, under config.contracts.<type>:

  • want.<consumer>.<instance>.request -- what a consumer asks for.
  • providers.<name> -- the providers that can answer the request.
  • defaultProvider -- the provider that answers when the consumer selects none. A want that no provider answers fails the evaluation.
  • providerRequests.<provider> -- the requests that a provider reads.
  • results.<consumer>.<instance> -- what the consumer reads back, for example a host, a port and a password file.

Each contract type defines the shape of its request and its result. The consumer and the provider agree only on these shapes, so a hosting provider can replace a provider without a change to the consumer.

Contracts across nodes

Each node of a deployment is a separate NixOS evaluation, so the nodes do not share config.contracts. When the consumer and the provider are on different nodes, crossNodeContract and crossNodeWant in core/lib/lib.nix evaluate the contract once, and give the result to each node. A contract that only gives coordinates, such as the address of a DNS server, does not need this. What a cross-node contract costs tells when to use one.

Where the code is

Part Location
The data model core/lib/data-model.nix
The resources core/resources/
The components core/components/
The environments core/setups/hosts-common.nix
The effects core/effects/
The contracts machinery nix/contracts/lib/contracts/module.nix
The contract types nix/contracts/lib/contracts/definitions/
The cross-node helpers core/lib/lib.nix

The contracts guide of the vendored contracts tree tells how to write a consumer, a provider and a new contract type.

Further reading