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 sqlite state store of deployments and build history.

Deployment identifiers are opaque to the API, formatted {username}/{pk}; the panel maps them to its Django User and Deployment models.

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).

Both specs name 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. It lists the production deployment and a local api-http-stack on http://127.0.0.1:8088. Try it out therefore works against the production API for the endpoints that need no credentials -- GET /schema is the useful one -- while anything else answers 403 without a bearer token, which the dropdown cannot supply for you. 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.

The AsyncAPI spec's host server variable likewise defaults to the deployment's Centrifugo name instead of a bare {host} placeholder. That name resolves inside the deployment only -- it is not published in the public zone -- and the rendered AsyncAPI page is static output with no try-it-out button, so this half is informational: it tells you what to connect to from a browser that is already talking to the panel.

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.

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.authz.callbackUrl

HTTP endpoint called per request for dynamic authorization. The api/ POSTs {procedure, scope, headers} and expects {allow: bool, sub?: string}. Empty disables the callback and allows every request – dev mode only.

Type: string

Default:

""

Declared by:

services.fediversity-api.centrifugo.apiKeyFile

Path to a file containing Centrifugo’s HTTP API admin key. The file must be readable by the fediversity-api service user.

Type: string

Default:

""

Declared by:

services.fediversity-api.centrifugo.apiUrl

Base HTTP URL of Centrifugo’s /api/* endpoint (e.g. http://panel:8090/api). Empty disables Centrifugo publishes (events are dropped).

Type: string

Default:

""

Declared by:

services.fediversity-api.deployment.attribute

The attribute pointing to the deployment’s dev shell (devShells.<system>.<ATTRIBUTE>).

Type: string

Default:

"operator-tf-incus-operator-all"

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.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/9pzxgnnqcis3xifbf4ynawzyk2aihab9-source"

Declared by:

services.fediversity-api.deployment.root_path

The path to the flake containing the deployment.

Type: absolute path

Default:

/nix/store/h3wk0nk1mmrva6na1g1z1csaaxygyp58-source

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.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. The one endpoint a docs reader can exercise without a token is GET /schema, which is unauthenticated anyway.

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.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 (nix/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.restart

systemd restart behavior

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

Default:

"always"

Declared by:

services.fediversity-api.trustedSourceUrls

Exact module_source repo URLs api/ will fetch and deploy. A submitted source whose url equals one of these, after tolerating a trailing .git// and any ?ref=/#, passes; anything else – including a sibling repo or a /../ traversal that merely shares a prefix – is rejected with HTTP 400 before fetchTree. This is the hosting provider’s trust lever, not the Fediversity project gatekeeping forks.

The default is our repo only. An explicit empty list [ ] allows any source. 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: