API

The Fediversity API, under api/, is the HTTP and pub/sub backend that front-ends (such as the panel) use to manage deployments. It wraps the stateless deployment programs of the data model with queued execution, progress reporting, and authorization.

Where it sits

The API sits between front-ends and the deployment machinery:

  • Front-ends submit an operator configuration over HTTP. The API validates it, enqueues a build, and runs the deployment through the effects backends (subprocess by default, or Windmill for job tracking).
  • It streams build progress and deployment-state changes back over Centrifugo WebSocket channels (fediversity.builds.<id> and fediversity.deployments.<id>), rather than making clients poll.
  • It maintains a small state store of deployments and build history: a SQLite file, or PostgreSQL when API_DATABASE_URL is set.

Deployment identifiers are opaque to the API. The panel uses the primary key of its Django Deployment model.

Wire surface

The API's contract is split across two standards-based, generated artifacts, so the documentation never drifts from the code:

  • HTTP RPC (submit, query, cancel, delete, schema, build list, logs) is described by an OpenAPI 3.1 spec generated from the FastAPI app. Browse it as Swagger UI, or build the spec with nix build ./api#packages.x86_64-linux.openapi, or the interactive Swagger UI bundle with nix build ./api#packages.x86_64-linux.swagger-ui and open result/index.html.
  • Centrifugo pub/sub events (build progress, deployment-state changes) are described by an AsyncAPI 3.0 spec generated from the pydantic event models. Browse the rendered AsyncAPI docs, or build the spec with nix build ./api#packages.x86_64-linux.asyncapi-spec, or the rendered docs with nix build ./api#packages.x86_64-linux.asyncapi-docs.

CI validates both specs (api-openapi-validate, api-asyncapi-validate).

The OpenAPI spec names a real host

The OpenAPI spec carries a servers block, so Swagger UI offers a host dropdown rather than resolving calls relative to whichever static host is serving the page. The published bundle lists the production deployment and a local api-http-stack on http://127.0.0.1:8088. The production api/ allows the Swagger host as a CORS origin for exactly this, and for nothing else: it is deliberately not on the allowlist of origins api/ may call back to check authorization. A running instance overrides servers with its own domain, so /openapi.json fetched from a deployment always advertises that deployment.

Each deployment serves its own documentation pages

A hosting provider set-up serves the Swagger UI and the rendered AsyncAPI spec on names of their own on the api node, by default swagger.<apex> and asyncapi.<apex> (the swaggerDomain and asyncapiDomain options of the api component). The overview page links both. These pages document the API that the node runs. The only server of their OpenAPI spec is the domain of the node, and the origin of the Swagger UI page is in the CORS allowlist of that api/, so Try it out sends its requests to that deployment.

The name of the Centrifugo is a property of the deployment, not of api/. When the node runs Centrifugo on a public name (the eventsDomain that a front-end gives the api component), the host server variable of its AsyncAPI page has that name as its default. Otherwise, and in the asyncapi-spec and asyncapi-docs packages, the variable is the bare {host} placeholder. A build of nix/asyncapi.nix with eventsHost fills it in.

Sending authorized requests from the Swagger UI

The spec declares an HTTP bearer scheme on each operation that needs authorization. Without a token, you can call only the public operations, such as GET /schema; the others answer 403. To call them:

  1. Get a token from the front-end. api/ does not issue tokens: it sends the token to the authorization callback of the front-end, which decides on the call. The demo panel gives a token on its account page (see the panel user guide).
  2. In the Swagger UI, select Authorize, paste the token and select Authorize again. The Swagger UI then sends the token as Authorization: Bearer <token> with each request.
  3. When the token expires, the calls answer 403 again. Get a new token and replace it under Authorize.

Database

The state store is a database the API creates itself: a SQLite file, or PostgreSQL when API_DATABASE_URL is set. At startup on PostgreSQL, the API copies the rows of an existing SQLite file into PostgreSQL one time, only while the PostgreSQL tables are empty. state.py holds the schema as literal DDL and applies it on every SQLite connection, or once per PostgreSQL connection pool, with no migration framework. A deployment row is the operator's last submitted configuration; a build row is one run of the deployment machinery against it, and a deployment's state is derived from its build history rather than stored. The db-diagram package opens a throwaway database through state.py and reads the schema back out of it, so the picture below shows what the API creates at the revision that built this page:

Design and operation

The conceptual design -- why the wire surface is split, the build-queue semantics, the backend-selection criteria and the comparison that led to choosing Windmill, the authorization callback model, and the error-code mapping -- is written up in api/specification.md.

The specifications tell what api must do, and the principles that apply to all changes in api/.

Running the API locally (the Terraform state backend, the HTTP stack, optional Windmill, the required incus setup, and the environment variables that configure it) is an operational concern maintained alongside the code: see api/README.md.

API tests run with nix build ./api#checks.x86_64-linux.api-python-tests; end-to-end deployment tests with nix build ./api#checks.x86_64-linux.apps-api.

Options

The services.fediversity-api NixOS options below configure an API deployment.

services.fediversity-api.enable

Whether to enable Fediversity API HTTP service.

Type: boolean

Default:

false

Example:

true

Declared by:

services.fediversity-api.auditHead.interval

When to send the head, as a systemd OnCalendar value.

Type: string

Default:

"hourly"

Declared by:

services.fediversity-api.auditHead.publishCommand

A command that sends the head of the audit chain to a store off the fleet. Each interval, the unit fediversity-api-audit-head runs fediversity-api-audit-verify --json and writes its output, one JSON object with events, seq, hash and time, to the stdin of this command. The command runs as the user of the service. A later head that does not continue an earlier one shows a changed or removed event, also when the writers of the database changed the chain too. null sends the head nowhere.

Type: null or string

Default:

null

Example:

"/run/current-system/sw/bin/logger -t audit-head"

Declared by:

services.fediversity-api.auditRetentionDays

The number of days that api/ keeps an audit event: who started an action on a deployment, and the result. An audit event holds the account key of the actor, which is personal data, so keep it only as long as necessary. 0 keeps all audit events.

Type: unsigned integer, meaning >=0

Default:

365

Declared by:

services.fediversity-api.authz.allowAll

Without callbackUrl, allow every request. For development and tests only. When callbackUrl is empty and this is false, the service refuses to start.

Type: boolean

Default:

false

Declared by:

services.fediversity-api.authz.callbackTokenFile

Path to a file that contains a static token that api/ sends to the callback as Authorization: Bearer <token>. The callback can then refuse requests that do not come from api/. Empty sends no token, unless the AUTHZ_CALLBACK_TOKEN environment variable holds one, for example from an environmentFiles entry. This file has precedence over that variable.

Type: string

Default:

""

Declared by:

services.fediversity-api.authz.callbackUrl

HTTP endpoint called per request for dynamic authorization. The api/ POSTs {procedure, scope, headers} and expects {allow: bool, sub?: string, deployment_ids?: string[]}. With deployment_ids, GET /deployments lists only those deployments. Empty disables the callback: the service then starts only if allowAll is set.

Type: string

Default:

""

Declared by:

services.fediversity-api.authz.serviceTokenFile

Path to a file that contains a static token for internal calls from a trusted backend, for example the hosting provider’s own backend. A request with Authorization: Bearer <token> goes to the callback with internal: true and without the token. The callback then decides on behalf of whom the call runs. Empty disables internal calls.

Type: string

Default:

""

Declared by:

services.fediversity-api.centrifugo.enable

Whether to enable a Centrifugo server for the events of api/.

Type: boolean

Default:

false

Example:

true

Declared by:

services.fediversity-api.centrifugo.allowedOrigins

Browser origins that may open a WebSocket. The Origin of the handshake is the document origin (the front-end), not the WebSocket host, and Centrifugo refuses a handshake from an origin not in the list.

Type: list of string

Default:

config.services.fediversity-api.frontendOrigins

Declared by:

services.fediversity-api.centrifugo.apiKeyFile

Path to a file containing Centrifugo’s HTTP API admin key. systemd gives it to the fediversity-api service as a credential. With centrifugo.enable, the server reads the same file.

Type: string

Default:

""

Declared by:

services.fediversity-api.centrifugo.apiUrl

Base HTTP URL of Centrifugo’s /api/* endpoint (e.g. http://events:8090/api). Empty disables Centrifugo publishes (events are dropped). With centrifugo.enable, the default is the HTTP API of that server.

Type: string

Default:

""

Declared by:

services.fediversity-api.centrifugo.domain

Public hostname browsers use to reach Centrifugo over WebSocket. When set, nginx terminates TLS on this name and proxies to Centrifugo. Empty leaves Centrifugo loopback-only.

The certificate is not configured here, as for domain: forceSSL is set and the TLS material comes from the ssl resource or from the user of this module.

A front-end that authorizes browsers by a session cookie needs a name that the cookie domain covers.

Type: string

Default:

""

Example:

"events.example.com"

Declared by:

services.fediversity-api.centrifugo.host

Bind address for Centrifugo’s HTTP server.

Type: string

Default:

"127.0.0.1"

Declared by:

services.fediversity-api.centrifugo.port

Bind port for Centrifugo’s HTTP server.

Type: 16 bit unsigned integer; between 0 and 65535 (both inclusive)

Default:

8090

Declared by:

services.fediversity-api.centrifugo.proxyBaseUrl

Base URL of the proxy views of the front-end. Centrifugo forwards the Cookie and Host headers of each connect, subscribe and publish to <proxyBaseUrl>/<verb>/, and the front-end allows or refuses it. null sets no proxy, so no client can connect.

Type: null or string

Default:

null

Example:

"http://127.0.0.1:8000/api/centrifugo"

Declared by:

services.fediversity-api.deployment.attribute

The attribute pointing to the deployment’s dev shell (devShells.<system>.<ATTRIBUTE>). There is no default: the hosting provider sets its deployment program.

Type: string

Declared by:

services.fediversity-api.deployment.destroyExecutable

The executable for destroy operations, as exposed inside the shell named attribute.

Type: string

Default:

"tf-destroy.sh"

Declared by:

services.fediversity-api.deployment.env

The deploy-env value-map forwarded verbatim into the render-eval subprocess. Its keys are the core-declared deploy-env key-set (core.lib.deployEnvKeys), which is exactly what core’s render eval consumes (SSH_PRIVATE_KEY_FILE, PUBLISHED_HOSTS/INTERNAL_HOSTS, ENVIRONMENTS, TF_HTTP_ADDRESS/BOOTSTRAP, NETBOX_*, …). api/ forwards these without interpreting them, so it names no effect var: a new effect adds its keys to its envKeys and this map grows with no api/ edit. The deployer sets whichever values apply.

Type: attribute set of string

Default:

{ }

Declared by:

services.fediversity-api.deployment.executable

The executable to trigger as exposed inside the shell named attribute.

Type: string

Default:

"tf-run.sh"

Declared by:

services.fediversity-api.deployment.migrationAttribute

The attribute pointing to the #78 migration mint dev shell (devShells.<system>.<ATTRIBUTE>), which reaches a running source’s Incus containers to mint each enabled app’s backup snapshot.

Type: string

Default:

"operator-migration-all"

Declared by:

services.fediversity-api.deployment.migrationExecutable

The executable for the migration mint, as exposed inside the shell named migrationAttribute.

Type: string

Default:

"mint-migration-snapshot.sh"

Declared by:

services.fediversity-api.deployment.moduleType

The type of the option module to expose to the user.

Type: optionType

Declared by:

services.fediversity-api.deployment.nixpkgs

The nixpkgs instance for NIX_PATH.

Type: string

Default:

"/nix/store/88mi762g8hxmwpd4w5yiiv3aah5sbgxs-source"

Declared by:

services.fediversity-api.deployment.openbao.address

The OpenBao address (VAULT_ADDR form) the api writes the state pointers of a migration import to, for the restore step of the operator deploy. Empty refuses a migration import that has state to restore.

Type: string

Default:

""

Declared by:

services.fediversity-api.deployment.openbao.caCertFile

The CA of the certificate of address. Empty uses the system trust store.

Type: string

Default:

""

Declared by:

services.fediversity-api.deployment.renamesLog

The accumulated rename harvest for moduleType, as { from; to; since; } entries oldest first. A rename is recoverable from the option tree only while the pinned tree carries its mkRenamedOptionModule marker, so the log is what lets the served table reach past the current pin; see core/lib/renames-for-source.nix.

The default is this tree’s own log, which pairs with the default moduleType. A moduleType layered on from outside declares renames no log of ours carries, which is a build error – handing the log in is the claim that it covers the type – so such a deployment supplies its own log, or null for none: no history, and no gate.

Type: null or (list of attribute set of anything)

Default:

lib.importJSON ../../core/renames-log.json

Example:

lib.importJSON ../../core/renames-log.json
++ [
  {
    from = [ "apps" ];
    to = [ "applications" ];
    since = 1;
  }
]

Declared by:

services.fediversity-api.deployment.root_path

The path to the flake containing the deployment. There is no default: the hosting provider sets its deployment program.

Type: absolute path

Declared by:

services.fediversity-api.deployment.secretEnvKeys

Which of the deploy-env variables this unit runs with hold a credential. Windmill writes a job’s arguments to its job table in clear and shows them to everyone who can open the job, so deployment.py drops these from the job it submits and the windmill worker reads its own copy of each from OpenBao instead (deploySecrets in core/components/hosting-provider/windmill/windmill.nix). The in-process subprocess route keeps them: it starts the deploy itself and writes no job row.

A name here that the windmill node does not render reaches the deploy as nothing at all, so the two are stated once and read by both (core/components/hosting-provider/deploy-credentials.nix).

Type: list of string

Default:

[ ]

Declared by:

services.fediversity-api.deployment.selfModuleSource

A fetchTree-compatible pin of api/'s own source revision, surfaced to the service as SELF_MODULE_SOURCE. When a deployment is submitted with no module_source, api/ freezes it to this pin rather than storing a bare null, so the “old” schema for the change view stays the one actually deployed under instead of floating to whatever api/ is later rebuilt against.

The revision is a property of the deploy, not the source tree – recording it in-tree would be self-referential, since writing the revision changes the tree and thus the revision – so it is threaded in here rather than baked into the package. The deployer must set it to the built revision, including a narHash so the fetch is hash-pinned; see api/specification.md. null keeps the floating behaviour, acceptable for a dirty local build whose working tree is not reproducible via fetchTree anyway.

Type: null or (attribute set of anything)

Default:

null

Example:

{
  narHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
  rev = "0000000000000000000000000000000000000000";
  type = "git";
  url = "https://git.fediversity.eu/fediversity/fediversity";
}

Declared by:

services.fediversity-api.deployment.stateAddressVars

Terraform variables that get the state address of an other deploy. For each <name> = <suffix>, a deploy gets TF_VAR_<name> = <base>-<suffix>, where <base> is the TF_HTTP_ADDRESS before the scope of the deployment. The scope moves the state address of the deploy, so a deployment program that reads the state of an other deploy gets that address here.

Type: attribute set of string

Default:

{ }

Example:

{
  peer_state_address = "peer";
}

Declared by:

services.fediversity-api.docs.asyncapiDomain

FQDN of the rendered AsyncAPI specification of the events of this api/, or null for no page. nginx serves the page on this name. The page only reads, so it needs no CORS entry. With centrifugo.enable and a centrifugo.domain, the page names that domain as the server of the events.

Requires domain. As for domain, the certificate comes from the ssl resource, or from the user of this module.

Type: null or string

Default:

null

Example:

"asyncapi.example.org"

Declared by:

services.fediversity-api.docs.swaggerDomain

FQDN of a Swagger UI page for this api/, or null for no page. nginx serves the page on this name. Its OpenAPI spec comes from the package of this node, with domain as its only server, so the “Try it out” button sends its requests to this api/. The origin of the page is added to docsOrigins, so the browser lets the page read the replies.

Requires domain. As for domain, the certificate comes from the ssl resource, or from the user of this module.

Type: null or string

Default:

null

Example:

"swagger.example.org"

Declared by:

services.fediversity-api.docsOrigins

Extra browser origins allowed to call api/ via CORS, without being trusted in the other direction. Deliberately not folded into frontendOrigins: that list doubles as the destroy-reconcile callback allowlist, and a static documentation host must never become a legal authorization oracle.

The use is a published Swagger UI bundle whose “Try it out” button issues browser calls to this api/. That is safe because CORS restricts browsers only – anyone holding a token could always call from outside one – and because allow_credentials is off, so a browser attaches no ambient cookies; api/ does no bearer validation of its own and answers a tokenless call with 403. Without a token, a docs reader can call only the public endpoints, such as GET /schema. With a token of the front-end, pasted under “Authorize”, the reader calls as the user of that token, as the front-end could.

docs.swaggerDomain adds the origin of its own page here.

Type: list of string

Default:

[ ]

Declared by:

services.fediversity-api.domain

api/'s browser-facing FQDN. When set, nginx terminates TLS on this name and proxies to the loopback-bound FastAPI server, giving api/ a browser-reachable origin: the panel’s deployment form fetches its schema and rename table straight from api/ (GET /schema, GET /schema/migrations), and the Deploy/Delete buttons call it directly too. Empty leaves api/ loopback-only – no vhost, no ingress.

The certificate is not configured here: forceSSL is set and the ssl resource writes sslCertificate/sslCertificateKey (and enableACME for a publicly-issued leaf) onto this vhost from the application’s tls.ssl.domains request, as it does for the panel. A standalone user of this module setting domain must supply that TLS material itself.

Type: string

Default:

""

Declared by:

services.fediversity-api.environmentFiles

EnvironmentFile= entries of the service, for example the runtime-discovered TF_VAR_image_fingerprint that then rides the windmill job payload. A leading - makes a file optional. A file must not set TF_VAR_incus_config_dir: systemd applies an EnvironmentFile after Environment=, so a stale path there would beat the StateDirectory one that the service sets.

Type: list of string

Default:

[ ]

Example:

[
  "-/var/lib/example/tf-env"
]

Declared by:

services.fediversity-api.frontendOrigins

Browser origins (scheme + host + optional port) allowed to call api/ directly via CORS. The front-end lives on a different registrable domain than api/ (white-labelling), so cross-origin requests need an explicit allowlist. Empty disables CORS (no browser-direct access).

This list doubles as the allowlist for the destroy reconcile callback: the browser supplies a callback URL on the DELETE request and api/ only POSTs it back, on destroy-success, when its origin is in this list. The set of front-ends api/ may call back is exactly the set trusted to call in, so it is one list. See api/specification.md (Two-store model and deletion).

Type: list of string

Default:

[ ]

Declared by:

services.fediversity-api.hostingConfig

The hosting provider’s curation of the deployment form, as data: the HOSTING_CONFIG model documented in api/specification.md, with its environments and effect-defaults sections. null leaves the form uncurated, offering every operator-facing field.

Taken as an attrset rather than a path so the values stay module-composable and no relative import has to survive a copy into the store; it is rendered to one JSON file here.

That one file reaches all three consumers that must agree – the build-time schema prune (api/package.nix’s hostingLeaves), the per-module_source schema re-eval (HOSTING_CONFIG in the unit environment), and the deploy eval (HOSTING_CONFIG in deploy_env) – so a leaf pruned from the form is exactly a leaf baked as a value, and the operator can neither see nor override it.

Type: null or (attribute set of anything)

Default:

null

Example:

{
  effect-defaults = {
    tf-incus-operator-hosts = {
      host = "10.0.100.1";
      port = 8443;
    };
  };
}

Declared by:

services.fediversity-api.http.listenAddr

Bind address for the FastAPI HTTP server. Loopback by default even when domain publishes api/ to browsers: nginx fronts it on the same node, so the FastAPI server itself stays off the network.

Type: string

Default:

"127.0.0.1"

Declared by:

services.fediversity-api.http.listenPort

Bind port for the FastAPI HTTP server. 8081 collides with terraform-backend’s metrics endpoint when both run on the same host; pick something further out by default.

Type: 16 bit unsigned integer; between 0 and 65535 (both inclusive)

Default:

8088

Declared by:

services.fediversity-api.pinEffect

Collapse the served deployment-method union to this single member: the served schema loses deployment-method entirely and carries that effect’s settings at its root, so the form has no method picker and renders those settings as ordinary fields. Names any union member – a deployment method or a standalone effect. The module-option counterpart of the PIN_EFFECT build variable documented in api/README.md; an undeclared name fails the build, as does a setting whose name a top-level option already takes.

null bakes the whole union, the form where the operator picks the method.

Type: null or string

Default:

null

Example:

"tf-incus-operator-hosts"

Declared by:

services.fediversity-api.registrarSnapshot

The snapshot of the registrar account that registrar-domains writes (services.fediversity-registrar-domains.snapshotFile). The unit fediversity-api-domain-renewals compares it with the domain events, and records the renewals and the releases of the domains of the deployments. A new snapshot starts the unit. null records no renewal and no release.

Type: null or string

Default:

null

Example:

"/run/registrar-domains/registrar.json"

Declared by:

services.fediversity-api.restart

systemd restart behavior

Type: one of “no”, “on-success”, “on-failure”, “on-abnormal”, “on-abort”, “always”

Default:

"always"

Declared by:

services.fediversity-api.schemaBuildTimeout

Deadline in seconds for one on-demand nix fetch-and-build of a module_source’s schema or rename table, described under “Cost and latency” in api/specification.md, and for the eval of the nodes of a submitted configuration. A cold build fetches the source and may evaluate IFD dependencies, so it takes minutes; overrunning this is killed and answered with a 504 fediversity.error.build_timeout.

This value also sizes the ingress: nginx must give a request at least as long as api/ may spend inside it, or its own 504 – which carries none of the CORS headers the app sets – replaces the app’s answer and the browser sees an opaque network error.

Type: positive integer, meaning >0

Default:

300

Declared by:

services.fediversity-api.trustedSourceUrls

Exact module_source repo URLs api/ will fetch and deploy. A submitted source passes when its url equals one of these, after tolerating a trailing .git//. A sibling repo or a /../ traversal that merely shares a prefix does not pass. This is the hosting provider’s trust lever, not the Fediversity project gatekeeping forks.

In a trusted repo, the source must also select a branch: a git source whose ref is absent, HEAD, a branch name or refs/heads/*, with no allRefs and no ?query or #fragment on the url. A rev must be a full commit id that the named branch (or, with no ref, any branch) contains; api/ asks the repo with git. Thus anyone who can open a pull request cannot deploy its refs/pull/* commits. Pushing a branch needs write access. api/ rejects any other source with HTTP 400 before fetchTree.

The default is our repo only. An explicit empty list [ ] allows any source, with no branch check. A null module_source (the built-in default module) is always allowed; it is not a caller-named fork.

Type: list of string

Default:

[
  "https://git.fediversity.eu/fediversity/fediversity"
]

Declared by:

services.fediversity-api.windmill.baseUrl

Base HTTP URL of the Windmill instance.

Type: string

Default:

""

Declared by:

services.fediversity-api.windmill.workspace

Windmill workspace identifier.

Type: string

Default:

"fediversity"

Declared by:

services.fediversity-api.zoneFollow.probeLabels

The labels below a deSEC zone that api follows after a migration export, which the follow also asks the new nameservers for when it copies the zone by queries: when no new nameserver gives a zone transfer or a walk of the NSEC chain. The copy then also has such a name that only the target holds. The default is the labels under which Fediversity writes records with the default settings.

Type: list of string

Default:

import ../../core/components/standard-labels.nix { inherit lib; }

Example:

[
  "mastodon"
  "_dmarc"
]

Declared by:

services.fediversity-api.zoneFollow.whoamiUrls

The HTTPS URLs that give the address of the client, as plain text or as JSON with data.ip. A migration export of a deployment whose zone the provider holds at deSEC puts the global addresses of api in the migration string, thus the target lets the follow in api transfer the zone. For each IP version, api takes the local address of its route to other networks. When that address is not global, for example behind NAT, the export asks these URLs in order, over that IP version, and takes the first global address of that version. The service then sees the address of api. An empty list asks no URL: the export then gives no address for that IP version.

Type: list of string

Default:

[
  "https://stat.ripe.net/data/whats-my-ip/data.json"
  "https://am.i.mullvad.net/ip"
]

Example:

[ ]

Declared by: