Run the API locally

The nix run and nix build commands in this section need the Nix features nix-command and flakes. See Prerequisites for how to enable them.

Every nix run .#... / nix build .#... invocation in this section runs from the api/ devShell, where .# resolves to the api flake; from the repo root write them as ./api#... (the root entrypoint is thin and only re-exports the deploy family -- see the top-level README.md).

There are two local setups. Pick Simple unless you specifically want to exercise the Windmill / Centrifugo paths.

Simple setup (synchronous, no Windmill/Centrifugo)

Deployments run as background subprocess threads inside api/; there is no job runner and no event bus. The panel reflects completion by polling api/ (see Run the panel locally). Leave all WINDMILL_* and CENTRIFUGO_* variables unset.

# terminal 1 (api/ devShell)
nix run .#terraform-backend-dev

# terminal 2 (api/ devShell)
export API_CORS_ORIGINS=http://127.0.0.1:8000,http://localhost:8000
export DEPLOYMENT_ROOT="$(git rev-parse --show-toplevel)"
export DEPLOYMENT_ATTRIBUTE=operator-tf-incus-operator-all
export DEPLOYMENT_STATE_ADDRESS_VARS='{"hp_state_address":"hosting-provider-addresses"}'
nix run .#api-http-stack

The variables in terminal 2 do these tasks:

  • API_CORS_ORIGINS: the browser origins of the panel. Without it, the browser blocks the deployment form; see CORS below.
  • DEPLOYMENT_*: the deployment program of the fediversity hosting provider; see Deployment program. api-http-stack refuses to start without a deployment program: set DEPLOYMENT_ROOT and DEPLOYMENT_ATTRIBUTE, or DEPLOY_CONFIG_FILE.

api-http-stack checks each call against the authz callback of the local panel, at http://127.0.0.1:8000/api/authz/, with the token in $XDG_RUNTIME_DIR/fediversity-authz/token. The panel must run, else api/ refuses all calls that need authorization; see Run the panel locally. To use api/ without the panel, also set API_AUTHZ_ALLOW_ALL=1: then api/ allows all calls. Outside of development, api/ refuses to start without AUTHZ_CALLBACK_URL.

The environment reference documents all other variables.

terraform-backend-dev listens on 127.0.0.1:8888 and persists state across restarts. api-http-stack automatically sets TF_HTTP_ADDRESS=http://127.0.0.1:8888/state/local/default so the API picks it up without additional configuration.

api-http-stack also persists its own deployment state DB ($XDG_RUNTIME_DIR/fediversity-api/state.db) across restarts, so a deployment made before a restart survives it -- restarting api/ (for example to rebuild it against a different source) no longer wipes deployment state. To start from a fresh DB, run with API_RESET_STATE=1:

API_RESET_STATE=1 nix run .#api-http-stack

When the panel form is in use, the browser fetches the schema and rename table (/schema, /schema/migrations) directly from api/. Both schema endpoints (GET /schema and the authenticated POST /schema/get_for) take an optional application -- such as mastodon or authelia -- which re-roots the response at that one application's own options instead of returning the whole operator form, so a consumer rendering or validating a single application does not have to walk the form itself:

curl -s 'http://127.0.0.1:8088/schema?application=peertube' | jq .schema

The result is self-contained ($schema carried over, $defs reduced to what the subtree references), and an undeclared name is a 400 listing the declared ones. It comes from core's provider-application-schemas output: baked for GET /schema, built per source for POST /schema/get_for. The rename table takes no such parameter (it is a flat list, not an option tree). See Serving one application's schema.

CORS

Those calls are cross-origin (the panel is served from :8000, api/ from :8088), so terminal 2 sets API_CORS_ORIGINS before api-http-stack to allow the browser to read the responses. The panel side sets the matching API_BROWSER_BASE_URL; see Run the panel locally. Match the origin to the host in the browser's address bar -- 127.0.0.1 and localhost are distinct origins to the browser. Leaving API_CORS_ORIGINS unset disables CORS, so the form's fetches return 200 but the browser blocks reading them.

Full setup (Windmill + Centrifugo)

Adds Windmill (deployment job tracking) and Centrifugo (live loading-screen updates over WebSocket). Run the two simple-setup terminals plus:

# terminal 3 (api/ devShell)
nix run .#windmill-dev

# terminal 4 (api/ devShell)
nix run .#centrifugo-dev

windmill-dev starts a local PostgreSQL instance and the Windmill server on http://localhost:8001. It writes a superadmin token to $XDG_RUNTIME_DIR/fediversity-windmill/token on first run.

centrifugo-dev listens on 127.0.0.1:8090 and writes an HTTP admin key to $XDG_RUNTIME_DIR/fediversity-centrifugo/api_key on first run. The api/ variant lets api/ publish, but allows no browser origin and has no proxy views. A front-end leaf runs its own variant through centrifugo-dev.override { frontendOrigins; proxyBaseUrl; frontendNotes; }.

Both apps print the env vars to set. Start them before api-http-stack, as the token and the key exist only after their first run. Then terminal 2 has the variables of the simple setup and the variables of the two apps:

# terminal 2 (api/ devShell)
export API_CORS_ORIGINS=http://127.0.0.1:8000,http://localhost:8000
export DEPLOYMENT_ROOT="$(git rev-parse --show-toplevel)"
export DEPLOYMENT_ATTRIBUTE=operator-tf-incus-operator-all
export DEPLOYMENT_STATE_ADDRESS_VARS='{"hp_state_address":"hosting-provider-addresses"}'
export WINDMILL_BASE_URL=http://localhost:8001
export WINDMILL_TOKEN="$(cat "$XDG_RUNTIME_DIR/fediversity-windmill/token")"
export CENTRIFUGO_API_URL=http://127.0.0.1:8090
export CENTRIFUGO_API_KEY_FILE="$XDG_RUNTIME_DIR/fediversity-centrifugo/api_key"
nix run .#api-http-stack

api-http-stack reads these variables when it starts, so nix run needs no --impure. Only the variables that change the build, PIN_EFFECT and HOSTING_CONFIG, need nix run --impure; see PIN_EFFECT.

The panel side sets CENTRIFUGO_BROWSER_WS_URL; see Run the panel locally for that and the host-consistency caveat (use 127.0.0.1 or localhost consistently for the panel and the ws url, or the session cookie is not shared and the WS connect silently fails).

With these set, deployment progress flows through live Centrifugo events and Windmill job tracking instead of the subprocess fallback and polling.

To also run the panel, follow the instructions in Run the panel locally.

The component reads configuration from environment variables. Every one of them -- what it does, its default, and which services.fediversity-api option derives it on NixOS -- is documented in the environment reference.

Deployment program

api/ has no default deployment program. The hosting provider names it, with these services.fediversity-api.deployment options:

  • root_path: the flake that contains the program.
  • attribute: the dev shell of the program (devShells.<system>.<attribute>).
  • stateAddressVars: Terraform variables that get the state address of an other deploy. api/ scopes the state address of each deploy, so a program cannot derive the address of an other deploy from its own address. For each <name> = <suffix>, api/ sets TF_VAR_<name> to <base>-<suffix>, where <base> is the TF_HTTP_ADDRESS before the scope.

The fediversity hosting provider sets these values in api/nix/component/api.nix:

Option Value
root_path the repository root
attribute operator-tf-incus-operator-all
stateAddressVars { hp_state_address = "hosting-provider-addresses"; }

The operator graph reads the addresses document of the hosting-provider deploy at hp_state_address to find the shared nodes. The front of the state backend lets the operator read only this document of that deploy. api-http-stack takes the same values from DEPLOYMENT_ROOT, DEPLOYMENT_ATTRIBUTE and DEPLOYMENT_STATE_ADDRESS_VARS (JSON).

Using a password-protected SSH key

Deployments authenticate to target hosts over SSH. By default the deploy path uses an explicit key file (the SSH_PRIVATE_KEY_FILE env var, forwarded through deploy-config.json's opaque deploy_env map). Because every SSH call runs with BatchMode=yes, that key file must be unencrypted -- a passphrase prompt would be suppressed and the connection would fail.

To deploy with a password-protected personal key, use ssh-agent instead: decrypt the key once with ssh-add, and SSH talks to the agent over SSH_AUTH_SOCK, so no passphrase is needed at deploy time.

# start an agent (skip if one is already running) and add the protected key
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519   # prompts once for the passphrase

# run the API (or panel) from this same shell so it inherits SSH_AUTH_SOCK,
# and leave SSH_PRIVATE_KEY_FILE / ssh_private_key_file unset
# (api/ devShell, or `./api#api-http-stack` from the repo root)
nix run .#api-http-stack

When no readable key file is configured but SSH_AUTH_SOCK is set, the deploy scripts omit -i and let the agent supply identities. With a key file present, the explicit-key behavior is unchanged (this is what production's agenix unprotected key relies on). The Windmill worker runs as a service, so an agent is only available there if the operator arranges a service-level SSH_AUTH_SOCK.