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>andfediversity.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_URLis 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 withnix build ./api#packages.x86_64-linux.swagger-uiand openresult/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 withnix 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:
- 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).
- 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. - 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: