hyperhive — swarm options

services.hyperhive.swarm.authelia.package

authelia package to run in the container. Defaults to nixpkgs’s; override to pin a specific upstream.

Type: package

Default:

pkgs.authelia

Declared by:

services.hyperhive.swarm.authelia.bridgePackage

swarm-authelia-bridge package — the only process allowed to write usersFile. Wired by default from this flake’s own package set (see flake.nix); override to run a different build.

Type: package

Default:

hyperhive.packages.${system}.swarm-authelia-bridge

Declared by:

services.hyperhive.swarm.authelia.bridgePort

TCP port swarm-authelia-bridge listens on, loopback-bound (127.0.0.1:${bridgePort}) — one above authelia’s own default port (9091), outside hyperhive’s other claimed ranges.

Reachable directly from this host’s other processes (this container shares the host netns, same as authelia’s own port) without going through the gateway — this is an internal service-to-service endpoint, not something meant to be exposed publicly.

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

Default:

9092

Declared by:

services.hyperhive.swarm.authelia.bridgeUrl

Where swarm-authelia-bridge answers, as seen from this host — correct only when a caller (swarm-controller) also runs on this host, the same co-location assumption swarm.nix’s clientSecretFile documents for its own cross-host case. null when this host doesn’t run swarm-authelia at all.

A split-host swarm has no automated delivery for this address: the operator points swarm-controller’s own option at wherever this host has made the bridge reachable (a firewall rule, a different bind address), the same manual-copy shape used throughout this codebase’s other cross-host cases.

Type: null or string (read only)

Default:

if enable then "http://127.0.0.1:${bridgePort}" else null

Declared by:

services.hyperhive.swarm.authelia.domain

Public hostname for the SSO provider — the sub-domain shape the forge and matrix already use, under the swarm’s domain because a swarm has one SSO provider. Must be the name browsers actually visit: it is the authelia_url the session cookie is validated against.

⚠️ Unlike the forge and matrix names, this one carries no migration pin: nothing depends on the previous auth.${services.hyperhive.domain} yet, so it moves outright.

Type: string

Default:

"auth.${services.hyperhive.swarm.domain}"

Example:

"login.example.com"

Declared by:

services.hyperhive.swarm.authelia.hiveClientPrefix

Prefix of the OAuth2 client id minted for each hive in services.hyperhive.swarm.hives — the client for hive alpha is hive-alpha. Read-only for the same reason as machine and unit: it is what this module produces, published so a consumer does not carry a second copy.

The consumer that matters is the queue’s auth-callout responder, which decides which hive a connection is by stripping this prefix off the introspected client id. Split the two spellings and every hive is denied — as a timeout, indistinguishable from a hive that simply has not reported.

Type: string (read only)

Default:

"hive-"

Declared by:

services.hyperhive.swarm.authelia.machine

Name of the nixos-container authelia runs in. Read-only: it is what this module declares, published so callers of systemctl -M and /var/lib/nixos-containers/<name> do not have to hardcode it.

Type: string (read only)

Default:

"swarm-authelia"

Declared by:

services.hyperhive.swarm.authelia.metricsPort

TCP port authelia serves its Prometheus metrics on, bound to loopback. Upstream’s default, kept so an operator reading authelia’s documentation finds what they expect.

A separate port from port because it is a separate listener with a different audience: the main one is proxied by the gateway and reachable from the swarm, this one is scraped by the collector on this host and by nothing else.

⚠️ Every swarm container shares the host network namespace, so two services defaulting to the same port do not conflict at build time — one simply loses at runtime, with nothing in any log. Check a new value against the others before changing this.

Note: Loopback means this endpoint is only reachable by a collector on the same host, so the scrape target is declared only when one is enabled here. Run the swarm’s collector elsewhere and authelia’s metrics are simply not collected — no error, and nothing in a log to say so. Making them reachable across hosts is a different piece of work: the endpoint would have to be published under a name, with a certificate and an audience.

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

Default:

9959

Declared by:

services.hyperhive.swarm.authelia.oidc.clients

OIDC relying parties this provider will issue tokens to. Declaring one turns the provider on; the default empty list leaves this module exactly as it was — a session provider and nothing else.

⚠️ There is deliberately no secret here. A client secret has two holders in two containers (authelia keeps a hash, the relying party the plaintext), and services.authelia.instances.<n>.settings is rendered into the nix store, which is world-readable and permanent. So this option carries only the parts that are safe to evaluate: the secret is minted on first boot and never passes through a nix expression. See docs/swarm/ for what goes where.

Type: list of (submodule)

Default:

[ ]

Declared by:

services.hyperhive.swarm.authelia.oidc.clients.*.accessTokenSignedResponseAlg

Signing algorithm for this client’s access tokens. null leaves authelia on its default, which issues an opaque token (authelia_at_…) — a database handle that carries no claims and means nothing to anyone but this provider.

Set RS256 when the resource server verifies the token itself rather than asking this provider about it: that yields an RFC 9068 JWT (at+jwt) carrying aud, iss and client_id, verifiable against /jwks.json with no round trip.

⚠️ This is what makes a token readable by an OIDC-verifying consumer at all. A resource server given an opaque token is not misconfigured — it is structurally unable to verify it, and says so in terms that point at the verifier rather than at the token’s format.

Type: null or one of “none”, “RS256”

Default:

null

Example:

"RS256"

Declared by:

services.hyperhive.swarm.authelia.oidc.clients.*.audience

Audiences (aud) this client is permitted to request a token for. Empty means it asks for none, which is the right answer for a client whose resource server does not distinguish callers.

⚠️ Registering an audience only permits it — the value lands in a token when the client asks for it at the token endpoint, and a client that does not send audience= receives a token with aud: [] however complete this list looks. Measured against authelia 4.39.20: the config reads exactly right and the resource server rejects every token, because a config that grants and a request that claims are two separate acts.

Requesting an audience that is not listed here is refused with invalid_target, which is what makes this usable as a boundary rather than a label: a client cannot mint a token for a resource slot that is not its own.

Type: list of string

Default:

[ ]

Example:

[
  "hive-alpha"
]

Declared by:

services.hyperhive.swarm.authelia.oidc.clients.*.bearerAuthz

Grant this client the authelia.bearer.authz scope, so it may present its access token to authelia’s authz endpoint and be authorised by an access_control rule — how a scraper reaches a service published behind the gateway.

A named capability rather than a free-form scopes list, for the same reason kind derives the rest: authelia refuses some scope/grant combinations outright (openid with client_credentials among them), and a list would make those combinations expressible again. This admits the one value that is legal here and nothing else.

Note: Setting this obliges two other options, and the assertions below enforce it. Authelia checks the same thing, but only in its preStart validator — which means a violation builds and deploys cleanly and then fails to restart, taking swarm SSO down. The assertions move that to evaluation, where a wrong value costs nothing.

Type: boolean

Default:

false

Example:

true

Declared by:

services.hyperhive.swarm.authelia.oidc.clients.*.description

Human-readable name, shown on authelia’s consent screen. This is the string a person reads when deciding whether to hand an application their identity, so it should name the application rather than the protocol.

Type: string

Example:

"HyperHive forge"

Declared by:

services.hyperhive.swarm.authelia.oidc.clients.*.id

OAuth2 client id, as the relying party knows itself.

Type: string

Example:

"forgejo"

Declared by:

services.hyperhive.swarm.authelia.oidc.clients.*.kind

Whether a human logs in through this client, or a daemon authenticates as itself.

interactive is the authorization-code flow: a browser is redirected, a person authenticates, the client receives an id-token. machine is client_credentials: there is nobody to redirect and no identity to assert but the client’s own, so it receives an access token and no id-token.

This is declared rather than inferred from an empty redirectUris, because authelia permits only the grants a client names — omitting grant_types yields authorization-code alone, and a daemon then fails at the token endpoint with unauthorized_client rather than at evaluation.

Type: one of “interactive”, “machine”

Default:

"interactive"

Example:

"machine"

Declared by:

services.hyperhive.swarm.authelia.oidc.clients.*.redirectUris

Exact callback URLs the provider will redirect to. Matched literally by authelia — a trailing-slash difference is a rejected login, not a warning.

Meaningless for kind = "machine", which is asserted rather than silently ignored.

Type: list of string

Default:

[ ]

Example:

[
  "https://forge.example.com/user/oauth2/authelia/callback"
]

Declared by:

services.hyperhive.swarm.authelia.oidc.clients.*.tokenEndpointAuthMethod

How this client proves its identity at the token endpoint. null leaves authelia on its own default (client_secret_basic), which is what every client that does not say otherwise gets.

Set it when the relying party’s implementation differs, because authelia enforces the registered method rather than accepting whatever arrives. tuwunel sends client_secret_post, and against a client registered for basic the result is a 401 from /api/oidc/token after a successful consent — the login looks like it worked right up to the last hop, and neither the redirect nor the secret is at fault.

⚠️ null is NOT accepted on a client with bearerAuthz. Measured against authelia 4.39.20: under that scope the method must be stated: omitting it is refused with must be configured as 'client_secret_basic', … but it's configured as an empty string. The sentence above is true of an ordinary client and false of that one, which is exactly how a reviewer reads this option and concludes the assertion below is wrong.

Type: null or one of “client_secret_basic”, “client_secret_post”, “client_secret_jwt”, “private_key_jwt”, “none”

Default:

null

Example:

"client_secret_post"

Declared by:

services.hyperhive.swarm.authelia.oidc.hiveIdentities

Mint one machine client per hive in services.hyperhive.swarm.hives, so each hive can authenticate to swarm services as itself.

Defaults to whether the swarm message queue is enabled, because that is the first service that needs a hive to prove who it is. It is an option rather than a hard-coded condition so a second consumer — the swarm telemetry collector — can turn it on without the queue, and so a swarm that wants the identities provisioned ahead of either can say so.

The clients are inert until something authenticates with them: each is a client id and a secret sitting on this host. What delivers a secret to a hive that is not this host is a separate problem and deliberately not solved here.

Type: boolean

Default:

services.hyperhive.deploy.nats.enable

Declared by:

services.hyperhive.swarm.authelia.port

TCP port authelia listens on. 9091 is upstream’s default and sits outside hyperhive’s claimed ranges (dashboard 7000, forge 3000, matrix 8008, every agent in 8100…8999 via FNV-1a hash).

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

Default:

9091

Declared by:

services.hyperhive.swarm.authelia.unit

authelia’s systemd unit inside the container. Read-only, and derived from the instance name exactly like the unit itself.

Type: string (read only)

Default:

"authelia-swarm.service"

Declared by:

services.hyperhive.swarm.authelia.url

Base URL clients are sent to for authentication — the half of this module that exists on every hive, not just the one running the container.

Defaults to this host’s own instance only when this module is the thing running it; in that case the URL is not a guess, it is where this module just put the container. Otherwise null, and a hive that federates with a swarm sets it explicitly to wherever the swarm’s authelia lives. Null means “no SSO configured” and consumers say so rather than inventing an address — an endpoint baked in as a fallback is one that resolves cleanly and points at the wrong machine.

Type: null or string

Default:

if enable then "https://${domain}" else null

Example:

"https://auth.example.com"

Declared by:

services.hyperhive.swarm.bao.domain

Name the store is reached on. A sibling of the swarm’s other service names, not a child of any hive domain: an authority whose nameConstraints permit one hive’s domain cannot issue for a sibling of it, so the shape of this name decides which authorities could ever sign for the store. That is a property of the name, not a choice of issuer — this module makes no such choice.

Type: string

Default:

"bao.${services.hyperhive.swarm.domain}"

Declared by:

services.hyperhive.swarm.bao.machine

Container name. Read-only: the name appears in host paths and in machinectl, so it is a fact other modules may read rather than a knob.

Type: string (read only)

Default:

"swarm-bao"

Declared by:

services.hyperhive.swarm.bao.port

TCP port the store listens on. Upstream’s own default, kept so an operator reading OpenBao documentation finds what they expect.

Swarm-wide because a client has to know it to reach the store, and the same port on every listener: which addresses the store answers on is the running host’s business (services.hyperhive.deploy.bao.extraListenAddresses), but which port it answers on is something the whole swarm agrees.

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

Default:

8200

Declared by:

services.hyperhive.swarm.ca.autoConfigure

Run the whole swarm CA on this one host: generate the swarm root when it is missing, and issue this hive’s CA under it.

services.hyperhive.deploy.singleHostSwarm turns this on as part of the all-on-one-box mode. Set it here directly to run the CA on a host that is not otherwise all-local.

Off by default, deliberately. A swarm’s services and its hives can live on different hosts, and this host has no way to tell whether it is the one holding the root — so the swarm CA is something an operator sets up, not something a host decides it is. Turn this on for an all-on-one-host deployment (dev boxes, single-hive swarms) and get the hierarchy for free.

With it off, both artifacts are operator-provided: the root under stateDir, and this hive’s CA under services.hyperhive.deploy.hive-controller.tls.stateDir. A hive given neither keeps the self-signed CA it has always had — it simply isn’t part of a swarm’s trust hierarchy, which is the correct outcome for a hive nobody has federated yet.

Type: boolean

Default:

false

Example:

true

Declared by:

services.hyperhive.swarm.ca.servicesValidityDays

Validity window of the swarm-services sub-CA in days (~5y). Deliberately far shorter than the root’s: this CA is meant to be re-issued — adding a swarm service changes its name constraints and rotates it — so a long window buys nothing, and a short one keeps the rotation path exercised rather than theoretical.

Rotating it is cheap in the way rotating the root is not: it touches only the swarm-service vhosts, and no peer hive holds it as an anchor.

Type: signed integer

Default:

1825

Declared by:

services.hyperhive.swarm.ca.stateDir

Host directory holding the swarm root CA: root.pem (the anchor, safe to distribute — copy it to this same path on every other host in the swarm) and root-key.pem (0600, the one file that must never reach the nix store or another host). The directory itself is 0700: nothing reads out of it but the hive CA issuance in hive-tls.nix.

Moving the swarm CA to its own host is a matter of moving this directory and setting autoConfigure = false here.

Type: string

Default:

"/var/lib/swarm-ca"

Declared by:

services.hyperhive.swarm.ca.validityDays

Validity window of the swarm root CA in days (default ~30y). Deliberately longer than services.hyperhive.deploy.hive-controller.tls.caValidityDays: the root must outlive the hive CAs it issues, or those chains expire out from under hives that are still perfectly happy with their own intermediate. Rotating a root is the one operation in this system with no partial-failure mode — it invalidates every peer at once, paced by the slowest peer’s rebuild — so it is never automatic and this window is meant to be uneventful.

Type: signed integer

Default:

10950

Declared by:

services.hyperhive.swarm.controller.package

swarm-controller package. Wired by default from this flake’s own package set (see flake.nix); override to run a different build.

Type: package

Default:

hyperhive.packages.${system}.swarm-controller

Declared by:

Quick links to swarm-wide services, surfaced by the swarm UI’s links menu (GET /api/links). Same shape and same zero-code-change-to-extend idea as hyperhive.dashboardLinks (nix/agent-modules/dashboard-links.nix), one level up: rather than one central hardcoded list, each service’s own module contributes its own entry when it is actually enabled on this host — swarm-authelia.nix, hive-matrix.nix and hive-forge/default.nix all do — the same list-merge idiom services.hyperhive.gateway.localNames already uses. A future service module can push its own entry the same way, and an operator can add arbitrary extra entries here directly; neither needs a swarm-controller or swarm-ui change.

Only meaningful on the host that actually runs the controller — entries contributed on any other host are computed but never read. In a swarm that splits swarm-authelia/hive-matrix/ hive-forge across hosts other than the controller’s, this list only reflects what is enabled locally; see each contributing module’s own activation condition.

Type: list of (submodule)

Default:

[ ]

Example:

[ { label = "Wiki"; icon = "📖"; url = "https://wiki.example.com/"; } ]

Declared by:

services.hyperhive.swarm.controller.links.*.icon

Optional icon emoji or short glyph.

Type: string

Default:

""

Declared by:

services.hyperhive.swarm.controller.links.*.label

Display label for the link.

Type: string

Declared by:

services.hyperhive.swarm.controller.links.*.url

Full URL.

Type: string

Declared by:

services.hyperhive.swarm.controller.queue.natsUrl

Where the controller reaches the swarm queue.

Empty means unset, which the assertion below refuses — a controller with no queue is not a lighter controller.

singleHostSwarm fills this in with loopback, because that address is only correct when the queue is on this host: its container shares the host netns. That derivation lives with the mode rather than here, so this option describes itself rather than a deployment shape.

Type: string

Default:

""

Example:

"nats://queue.example.com:4222"

Declared by:

services.hyperhive.swarm.controller.queue.tokenEndpoint

The OIDC token endpoint the controller mints its own access token from.

Derived from swarm.authelia.url, which is the half of the authelia module that exists on every host — so this is already correct for a remote provider as long as that URL is set.

Type: string

Default:

"${swarm.authelia.url}/api/oidc/token"

Declared by:

services.hyperhive.swarm.controller.queueClientId

The OAuth2 client id the controller presents to the swarm queue. Read-only: it is what this module registers, published so the auth-callout responder can be told which client may read every hive’s key without repeating the string.

The responder decides that from a client id, and a client id it does not recognise is denied. A denial reaches a NATS client as a timeout rather than an error, and a controller that cannot read looks exactly like a swarm where no hive has reported yet — so a drift between these two spellings is invisible at the point it is introduced and misattributed everywhere it shows up.

Type: string (read only)

Default:

"swarm-controller"

Declared by:

services.hyperhive.swarm.controller.staleAfterSeconds

How old a hive’s last status snapshot may be before GET /api/hives/status reports it as stale rather than fresh.

This is a statement about how often hives publish, not about how patient a reader is — set it above the publishing cadence or every hive reads stale between offers. It is an option and not a constant precisely because that cadence is a property of the deployment.

Freshness is derived when the endpoint is read, never stored, so changing this takes effect for the next request; no hive has to re-publish anything.

Type: positive integer, meaning >0

Default:

120

Declared by:

services.hyperhive.swarm.controller.swarmctlPackage

swarmctl package — the swarm operator’s CLI, installed on this host alongside the daemon and wrapped with the paths it needs.

A separate option from package rather than a second binary in the same derivation: the CLI runs as root and acts directly, the daemon runs unprivileged and serves a socket, and pinning one without the other is a thing an operator may legitimately want.

Type: package

Default:

hyperhive.packages.${system}.swarmctl

Declared by:

services.hyperhive.swarm.domain

DNS domain of the wider swarm this hive belongs to. Each hive occupies its own sub-domain of it, which is why every entry in services.hyperhive.swarm.hives defaults its domain to <name>.<swarm.domain> — set this plus services.hyperhive.hiveName, list the hives by name, and no hive in the swarm states an address at all.

Required when services.hyperhive.enable, and deliberately not defaulted: there is no fallback worth having. A guessed swarm domain is a wrong hostname that evaluates cleanly and deploys, which is worse than an eval failure telling an operator to write down the one address their swarm answers to. Upgrading past this costs one line, once.

Type: null or string

Default:

null

Example:

"darkest.space"

Declared by:

services.hyperhive.swarm.forge.package

Forgejo package to run inside the container. Defaults to pkgs.forgejo (the latest release line) rather than the nixpkgs-module default of pkgs.forgejo-lts, because LTS lags far behind on schema and the DB easily ends up “newer than the binary” if the operator ever ran a non-LTS forgejo against the same state dir. Override to pkgs.forgejo-lts if you actively want the slower release train.

Type: package

Default:

pkgs.forgejo

Declared by:

services.hyperhive.swarm.forge.domain

Public hostname for the forge. Doubles as both the forgejo DOMAIN setting (clone URLs forgejo advertises) AND the gateway vhost server-name when deploy.forgejo.behindGateway = true (sub-domain routing — see docs/networking/gateway.md).

Defaults to forge.${services.hyperhive.swarm.domain} — the swarm’s domain, not this hive’s, because a swarm runs one forge that every hive in it talks to.

⚠️ A deployment that was running before this moved keeps its current name by pinning it here: forge.${services.hyperhive.domain}, which is exactly what the old default rendered. Certificates follow either way: the swarm-services sub-CA is name-constrained to the configured names (see ./swarm-ca.nix), not to a fixed tree.

Set to a full hostname (git.example.com, forge.internal.lan, etc.) for a bespoke vhost shape — the full domain goes here, no separate sub-domain-label option.

Type: string

Default:

"forge.${services.hyperhive.swarm.domain}"

Example:

"git.example.com"

Declared by:

services.hyperhive.swarm.forge.httpPort

TCP port the forge serves HTTP on. Default 3000 sits outside hyperhive’s claimed ranges (dashboard 7000, every agent in 8100…8999 via FNV-1a hash). Change this if you already have another forgejo bound to 3000.

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

Default:

3000

Declared by:

services.hyperhive.swarm.forge.publicUrl

Browser-facing forge URL the dashboard uses to build clickable forge links (the H0M3 Forge tile, per-agent-row forge links, the approval-queue’s “review PR on forge” link) — sourced into every agent container + hive-c0re as HIVE_FORGE_PUBLIC_URL.

Defaults to https://${cfg.domain} when deploy.forgejo.behindGateway = true (the gateway vhost is genuinely reachable at that URL) and null otherwise. When null, the dashboard hides forge links rather than guessing one — see docs/web-ui/dashboard.md::H0M3 page for the rationale (a link built from the operator’s own browser hostname + a container port is only an accident away from wrong on any deployment that isn’t plain localhost).

Set this explicitly if deploy.forgejo.behindGateway = false and the forge is still reachable at a stable URL you want linked from the dashboard (e.g. http://<lan-host>:${toString cfg.httpPort} for an all-LAN deployment) — leaving it unset there means the dashboard’s forge links are simply absent, not broken.

Type: null or string

Default:

if behindGateway then "https://${domain}" else null

Example:

"https://forge.example.com"

Declared by:

services.hyperhive.swarm.forge.rootUrl

Override the auto-derived forgejo ROOT_URL. When null (default), ROOT_URL is derived from cfg.domain + gateway state, including the scheme:

The TLS scheme is derived automatically now, so you only need to set this for a genuinely bespoke shape (e.g. an external reverse proxy on a different host/path). Must end with / per forgejo’s ROOT_URL contract.

Type: null or string

Default:

null

Example:

"https://forge.example.com/"

Declared by:

services.hyperhive.swarm.forge.sshPort

TCP port the forge’s built-in SSH server listens on. Kept off 22 so it doesn’t clash with the host’s openssh. Agents push with ssh -p <sshPort> git@<domain>:<owner>/<repo>.git.

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

Default:

2222

Declared by:

services.hyperhive.swarm.forge.sso.clientId

OAuth2 client id this forge identifies itself with. Must match the id of the corresponding entry in services.hyperhive.swarm.authelia.oidc.clients.

Type: string

Default:

"forgejo"

Declared by:

services.hyperhive.swarm.grafana.package

Grafana package to run.

Type: package

Default:

pkgs.grafana

Declared by:

services.hyperhive.swarm.grafana.domain

Name the gateway serves this on. A sibling of the swarm’s other service names, so the swarm-services sub-CA can issue for it — see hive-tls.nix for why a service name being a sibling rather than a child decides which CA may sign it.

⚠️ Changing this changes the OAuth redirect URI, which authelia matches exactly. Both sides move together because both derive from this option; an operator who pins one by hand breaks the login.

Type: string

Default:

"grafana.${services.hyperhive.swarm.domain}"

Declared by:

services.hyperhive.swarm.grafana.machine

Container name. Read-only: the name appears in host paths and in machinectl, so it is a fact other modules may read rather than a knob.

Type: string (read only)

Default:

"swarm-grafana"

Declared by:

services.hyperhive.swarm.grafana.metricsPort

Loopback port on which the gateway’s nginx re-serves Grafana’s /metrics, and nothing else, so the swarm’s collector can scrape it.

⚠️ This is nginx’s port, not Grafana’s. Grafana still claims none — see services.hyperhive.deploy.grafana.socketDir for why that matters. A prometheus scrape target is a host:port, and it cannot address a unix socket; rather than undo the socket decision, the one endpoint a scraper needs gets a listener of its own.

Bound to loopback and unauthenticated, which is the same posture every other entry in services.hyperhive.swarm.otel.scrapeTargets has: those targets are trusted by proximity rather than by credential. Deliberately not the published grafana.<domain> vhost, which would put an authorization decision in front of a scrape.

The number itself is arbitrary and free today; state/eval-port-collisions.sh is what keeps it that way, since a second claim on a port in this shared namespace produces no bind error and nothing in any log.

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

Default:

9095

Declared by:

services.hyperhive.swarm.grafana.oidc.clientId

The authelia OIDC client id. Names the application rather than the protocol, per the convention in services.hyperhive.swarm.authelia.oidc.clients.

Type: string

Default:

"swarm-grafana"

Declared by:

services.hyperhive.swarm.grafana.oidc.role

Grafana org role every SSO user is assigned.

Admin by default, and that is a considered default rather than a permissive one: the login form is disabled whenever SSO is configured, so this is the only way anyone reaches Grafana — a Viewer default would produce a swarm nobody can administer. Passing authelia already means being an operator of this swarm; its user store is the small, swarmctl-managed one.

Lower it if a swarm ever grows read-only operators, which is a one-line change here.

Type: one of “Viewer”, “Editor”, “Admin”

Default:

"Admin"

Example:

"Editor"

Declared by:

services.hyperhive.swarm.hives

Every hive in this swarm, keyed by hiveNameincluding this host’s own hive. The same attrset is meant to be identical on every host in the swarm, so it can be written once and shared; services.hyperhive.hiveName is what makes a given host read it as “me and four others” rather than “five peers”.

Each entry needs no fields at all in the conventional case: a hive’s domain defaults to <name>.<swarm.domain>, so the whole directory is usually a list of names.

It must contain an entry for hiveName, which is asserted — this host’s own address is read out of it (it is where services.hyperhive.domain derives from), and a hive that lists everyone but itself would otherwise derive its own peer set as everything and peer with itself.

Type: attribute set of (submodule)

Default:

{ }

Example:

{
  edge = {
    domain = "edge.elsewhere.example";
    wireguardAddress = "10.100.0.2/32";
  };
  pr1ma = {
    wireguardAddress = "10.100.0.1/32";
    wireguardEndpoint = "203.0.113.1:51820";
  };
}

Declared by:

services.hyperhive.swarm.hives.<name>.domain

Public DNS domain this hive occupies — used for swarm-controller’s hive roster, agent identity (qualified agent@domain labels), and Matrix federation discovery.

Defaults to <name>.<swarm.domain>, the convention every hive in a swarm follows, so a conventional directory is names only. Set it for a hive that is addressed by something else.

Type: string

Default:

"${name}.${services.hyperhive.swarm.domain}"

Example:

"lab.example.com"

Declared by:

services.hyperhive.swarm.hives.<name>.wireguardAddress

IP address (with prefix) of this hive’s host on the WireGuard mesh. Used as the allowedIPs for its WireGuard config entry (./swarm-wireguard.nix), so intra-swarm traffic can route over the mesh address rather than the public domain. Required to include a hive in the mesh (entries missing this field are silently excluded from wg-hive).

Type: null or string

Default:

null

Example:

"10.100.0.2/32"

Declared by:

services.hyperhive.swarm.hives.<name>.wireguardEndpoint

WireGuard endpoint for this hive in host:port form. Null = this hive has no reachable endpoint, so the tunnel is initiated from the other side.

Reads like a fact about the relationship and is not: it says whether this hive can be dialled, which every other hive in the swarm needs the same answer to.

Type: null or string

Default:

null

Example:

"203.0.113.1:51820"

Declared by:

services.hyperhive.swarm.hives.<name>.wireguardPublicKey

WireGuard public key for this hive’s host. Required when services.hyperhive.deploy.wireguard.enable = true and you want this hive reachable over the mesh. Null = TLS- only peering (public internet, no mesh tunnel).

Type: null or string

Default:

null

Example:

"base64pubkey="

Declared by:

services.hyperhive.swarm.matrix.package

matrix-tuwunel package to run inside the container. Defaults to nixpkgs’s pkgs.matrix-tuwunel. Override to pin a specific upstream if you need an unreleased feature.

Type: package

Default:

pkgs.matrix-tuwunel

Declared by:

services.hyperhive.swarm.matrix.allowEncryption

Server-side switch for matrix end-to-end encryption — sets tuwunel’s allow_encryption. Off by default: on the hive-internal homeserver the operator already controls the transport, so server E2EE adds key-management overhead (cross-signing, device verification, undecryptable-message recovery) without a clear threat-model win for the common single-hive case. Turn on when agents join encrypted rooms on external / federated homeservers, or when the operator wants message contents opaque to the homeserver admin. Independent of the agent matrix client, which always supports decryption so it can read encrypted rooms it is invited to regardless of this flag; this option only governs whether THIS homeserver permits room encryption.

Type: boolean

Default:

false

Declared by:

services.hyperhive.swarm.matrix.apiUrl

Client-server API base URL hive-c0re itself uses to provision matrix (register agent users, create the hive space and chat room, invite members). Distinct from the agent-facing hyperhive.matrix.url, which is the gateway vhost handed to each agent’s hive-matrix-daemon.

Defaults to the loopback listener only when this module is the thing running tuwunel — in that case the address is not a guess, it is where this module just put the container. Set it explicitly (with enable = false) when the homeserver runs on another machine; “everything on one host” is a special case of the full deployment, not the assumption.

null means hive-c0re has no homeserver to provision against and matrix provisioning no-ops. There is deliberately no fallback compiled into the daemon: an address baked into the binary is one that builds fine and then talks to the wrong machine.

Type: null or string

Default:

if services.hyperhive.deploy.matrix.enable
then "http://127.0.0.1:${toString services.hyperhive.swarm.matrix.httpPort}"
else null

Example:

"https://matrix.example.com"

Declared by:

services.hyperhive.swarm.matrix.gatewayHost

Public hostname for the matrix homeserver behind the gateway. Defaults to chat.${services.hyperhive.swarm.domain} — the swarm’s domain, because a swarm runs one homeserver. Set to null to skip the gateway vhost (tuwunel stays direct on httpPort). See docs/networking/gateway.md for the vhost map + matrix discovery flow, and the federation port-8448 caveat at the bottom of that doc.

⚠️ gatewayHost and serverName are different things, and they carry very different costs. gatewayHost is the API listener hostname (where nginx proxies /_matrix/*) and is free to change: it is a routing detail clients rediscover through .well-known. serverName is the matrix-identifier domain embedded irrevocably in every user and room id — adopting a new one is a different homeserver, not a rename. Both defaults now sit under the swarm domain, but only this one is safe to move on a running deployment.

A deployment that was running before this moved keeps its current name by pinning matrix.${services.hyperhive.domain} here — exactly what the old default rendered.

Type: null or string

Default:

"chat.${services.hyperhive.swarm.domain}"

Example:

"matrix.example.com"

Declared by:

services.hyperhive.swarm.matrix.gui.package

Static web client dist served at gatewayHost’s vhost (chat.<swarm-domain> by default). Override to swap fluffychat for hydrogen-web, cinny, element-web, or an out-of-tree dist — any replacement is mounted at the sub-domain root with the upstream-default <base href "/">, no sub-path gymnastics needed.

Type: package

Default: pkgs.fluffychat-web with a postInstall patch that adds the three files flutter341.buildFlutterApplication skips.

Declared by:

services.hyperhive.swarm.matrix.httpPort

TCP port tuwunel serves the matrix client-server API on. Default 8008 is the matrix-spec well-known port. Sits outside hyperhive’s claimed ranges (dashboard 7000, every agent in 8100…8999 via FNV-1a hash). Federation listens on federationPort separately.

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

Default:

8008

Declared by:

services.hyperhive.swarm.matrix.serverName

Matrix server_name — the host part of every user ID (@argus:<server_name>) and room ID minted on this homeserver. CRITICAL: must be stable from day one because it’s embedded irrevocably in the identifiers.

Defaults to services.hyperhive.swarm.domain (the bare swarm domain), because a swarm runs one homeserver — tying its identity to a single hive’s domain would make relocating the container between hives look like a different homeserver. Combined with the .well-known/matrix/{client,server} routes the gateway serves at that domain, clients auto-discover the actual matrix endpoint without needing a subdomain. Override here only if you need a different server_name shape (e.g. chat.example.org for a bespoke hostname).

Breaking change, and the one on this page that cannot be undone by rebuilding. This default has now moved twice — from matrix.${services.hyperhive.domain}, then to the bare hive domain, and now to the swarm domain. Every existing homeserver must pin whichever value it already minted ids under, e.g.

services.hyperhive.swarm.matrix.serverName =
  config.services.hyperhive.domain;   # or "matrix.${…domain}"

before rebuilding. Adopting a new server_name does not rename the old users and rooms — it strands them, because their ids still name a homeserver that no longer answers.

Type: null or string

Default:

null

Example:

"chat.example.org"

Declared by:

services.hyperhive.swarm.matrix.sso.clientId

OAuth2 client id this homeserver identifies itself with. Must match the id of the corresponding entry in services.hyperhive.swarm.authelia.oidc.clients.

The secret it pairs with is a host path, so it lives at services.hyperhive.deploy.matrix.sso.clientSecretFile.

Type: string

Default:

"tuwunel"

Declared by:

services.hyperhive.swarm.name

Human-readable name of the wider swarm this hive belongs to. Hives at different DNS domains can share a swarm name when they federate together. Exposed to agents as HYPERHIVE_SWARM_NAME; surfaced in the dashboard chrome and per-agent system prompt when set.

Type: null or string

Default:

null

Example:

"constellat1on"

Declared by:

services.hyperhive.swarm.nats.authPackage

The auth-callout responder package.

⚠️ Named authPackage, not package, on purpose: this module deliberately has no package option for the server itself (see the note above — upstream’s services.nats resolves pkgs.nats-server on its own), so a bare package here would read as “the NATS package” and mean something else entirely.

Type: package

Default:

hyperhive.packages.${system}.swarm-nats-auth

Declared by:

services.hyperhive.swarm.nats.clientId

OAuth2 client id the queue’s authentication path identifies itself with. Must match the id of the corresponding entry in services.hyperhive.swarm.authelia.oidc.clients — which this module contributes for you when both run on this host.

Type: string

Default:

"swarm-nats"

Declared by:

services.hyperhive.swarm.nats.metricsPort

Port the Prometheus exporter serves NATS’s metrics on, bound to loopback for the swarm collector to scrape.

⚠️ This and monitorPort are two more claims on a port space every swarm container shares — they run in this container but privateNetwork = false, so a collision with any other hyperhive service is a runtime coin toss over which process gets the port, with nothing in any log saying so. Both defaults are upstream’s own (nats-server 8222, prometheus-nats-exporter 7777) and neither is claimed elsewhere in this repo, checked when they were added.

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

Default:

7777

Declared by:

services.hyperhive.swarm.nats.monitorPort

Port NATS serves its monitoring endpoint on, bound to loopback.

Not a metrics endpoint: the server has no Prometheus format of its own. This serves /varz, /connz, /routez as JSON, and the exporter below is what translates it — which is why enabling the exporter without this produces a process that starts cleanly and scrapes nothing.

⚠️ Loopback, and the exporter is the only intended reader. The endpoint is unauthenticated and /connz names every connected client, so the address it binds is the whole access control. Do not widen it, and do not put it behind a gateway vhost expecting that to add one.

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

Default:

8222

Declared by:

services.hyperhive.swarm.nats.port

TCP port the queue listens on. 4222 is upstream’s default and sits outside hyperhive’s claimed ranges (dashboard 7000, forge 3000, matrix 8008, every agent in 8100…8999 via FNV-1a hash).

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

Default:

4222

Declared by:

services.hyperhive.swarm.otel.clientId

OAuth2 client id this collector authenticates as. Published so authelia’s access_control rules can name it without carrying a second copy of the string, exactly as services.hyperhive.swarm.authelia.hiveClientPrefix is published for the queue’s responder.

Two spellings drifting apart is not a build failure: authelia refuses a rule naming an unregistered client in its startup validator, so the swarm’s SSO service fails to restart — long after the change that caused it evaluated cleanly.

Type: string (read only)

Default:

"swarm-collector"

Declared by:

services.hyperhive.swarm.otel.domain

Name the gateway serves this on. A sibling of the swarm’s other service names, so the swarm-services sub-CA can issue for it — see hive-tls.nix for why a service name being a sibling rather than a child decides which CA may sign it.

This is what the hive tier’s exporter reaches — the hive collector is a plain producer against this name exactly like every other client of a swarm service, resolved locally by dnsmasq on a co-located host and over the real network otherwise. There is no separate loopback-vs-remote knob to get wrong: swarm-nats is the deliberate exception to this pattern (its cross-hive reach is the wireguard mesh, not the gateway), everything else in this swarm addresses its siblings by name.

Type: string

Default:

"otel.${services.hyperhive.swarm.domain}"

Declared by:

services.hyperhive.swarm.otel.journaldUnits

systemd units whose journal this collector ships to the swarm’s log store. Nothing outside this list is collected.

Every module that defines a unit worth reading swarm-wide adds its own names here, rather than one list naming them all: a service that is not running contributes nothing, and a service added later arrives with its units already declared. A central list would be a second place that has to know which services exist, and it would go stale in the direction that hides a service’s logs rather than the one that shows too many.

Names are matched as journald _SYSTEMD_UNIT values, so an in-container unit is named exactly as it is inside its container — authelia, not container@swarm-authelia.

⚠️ A name that matches nothing is not an error anywhere. The unit simply never appears in the store, which looks the same as a service that had nothing to say.

Type: list of string

Default:

[ ]

Example:

[
  "nginx"
]

Declared by:

services.hyperhive.swarm.otel.machine

Name of the nixos-container this collector runs in — also the machinectl name, so other modules may read it rather than repeating the literal.

Type: string (read only)

Default:

"swarm-otel"

Declared by:

services.hyperhive.swarm.otel.port

First port of this collector’s receiver range. Every hive in services.hyperhive.swarm.hives gets its own authenticated receiver — that is what makes the hive label unforgeable — so the range is one port per hive, starting here, in sorted-name order.

⚠️ Internal. No client is ever told a port: a hive reaches its own receiver as https://${domain}/<hive>, and the gateway routes on that path. So adding a hive, which renumbers the ones after it, is harmless — nginx is rendered from this same evaluation and moves with it.

⚠️ Deliberately not 4318, the OTLP/HTTP default, because the hive tier already uses it (services.hyperhive.otel.collector.port) and every swarm container shares the host’s network namespace. Two listeners claiming one port on one host is not a build failure — it is a runtime coin toss over which one gets it, with nothing in any log saying so. The same collision cost a release when grafana and the forge both defaulted to 3000. The assertions below check the whole derived range against every port this module and the hive tier declare, which is as far as a module can see.

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

Default:

4319

Declared by:

services.hyperhive.swarm.otel.producerName

The swarm-tier producer’s own component/path/authenticator name — the same reserved literal this module’s collector components (otlp/swarm, oidc/swarm, resource/swarm, metrics/swarm) are already named after internally. Published as an option so a sibling module (swarm-controller.nix) can address the receiver by name — https://${domain}/${producerName}/ — instead of repeating the string. Read-only for the same reason clientId/machine/domain are: two spellings of a name that has to match on both ends is a mismatch waiting to happen.

Type: string (read only)

Default:

"swarm"

Declared by:

services.hyperhive.swarm.otel.producerPort

Port the swarm tier’s own OTLP/HTTP receiver for a swarm-level producer (swarm-controller’s vcs/jobq counters today) listens on, at 127.0.0.1. Reached through the gateway at https://${domain}/${producerName}/, same shape as a hive’s own receiver — a swarm-level producer may not share a host with this collector (mara, on the swarm-controller topology: “the swarm services dont have to run on the same host as the swarm controller”), so loopback-only reachability is not a supported shape here any more than it is for a hive.

Authenticated by oidc/${producerName}, checking for the audience services.hyperhive.swarm.controller.queueClientId requests — not the per-hive hiveClientPrefix namespace, because a swarm-level producer is explicitly not a hive (see that option’s own doc comment). The swarm label this receiver’s samples get is still the one the receiver’s own existence establishes, same as before — there is no hive identity being forged or attributed here, just a caller proving it is the one principal allowed to push into this pipeline.

⚠️ Deliberately NOT derived from port + (number of hives): that range grows every time a hive is added, and a fixed offset from it would silently start colliding once the hive count caught up. Kept as its own reserved value instead, and the assertion below still catches a real collision (including one hive growth eventually causes) rather than starting a collector that quietly drops one receiver’s samples.

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

Default:

4390

Declared by:

services.hyperhive.swarm.otel.publishedScrapeTargets

Prometheus endpoints this collector scrapes by name, with a credential, as <job name> = "<url>".

A service module declares its own entry, from its own module, the same way it does for scrapeTargets — and the collector’s OAuth2 client derives its permitted audiences from these URLs, so a target and the authorisation to reach it are one declaration. Two lists that must agree would be a drift to maintain, and its failure mode is the bad one: a target whose audience was forgotten authenticates against nothing and reads as a scrape failure rather than a config mistake.

⚠️ A full URL, not host:port. Authelia validates a bearer token against the address being requested, so the string here is also the audience the token is minted for; a near miss (a trailing slash, http for https) presents as a valid token rejected at the target, several layers from its cause.

⚠️ Deliberately separate from scrapeTargets. An entry there is trusted because the scraper and the target share a host — that option is loopback-and-unauthenticated by contract. An entry here is trusted because it presents a credential. One shape for both would leave a reader unable to tell which of those a given target relies on.

Type: attribute set of string

Default:

{ }

Example:

{ forge = "https://forge.example.com/metrics"; }

Declared by:

services.hyperhive.swarm.otel.scrapeTargets

Prometheus exposition endpoints this collector scrapes, as <job name> = "<host>:<port>".

A path and query may follow the port — "127.0.0.1:8202/v1/sys/metrics?format=prometheus" — for an exporter that does not serve /metrics. Both are optional and omitted when absent, so a bare host:port is scraped exactly as before.

A service declares its own entry, from its own module, under its own enable. Do not assemble the list here: an entry then exists only where the service that named it runs, so a target is never declared on a host that does not serve it.

⚠️ That constrains the TARGET, not the SCRAPER, and the difference is a silent gap. Nothing here puts the collector on the same host. Services are co-located by defaultswarm-required-services.nix derives their enable flags from one lib.mkDefault — not by construction, and an operator may split them.

When they are split the target is simply never scraped: the service’s host declares an entry no local collector reads, and the collector’s host never enabled that service so has no entry at all. No error, no eval failure, no warning.

No assertion can catch this. Two hosts are separate NixOS evaluations with no shared context, so neither can see what the other runs. A service that would be seriously wrong to lose should say so in its own contribution, because saying it is the only mechanism available.

Samples land in a swarm-level pipeline that stamps swarm and never hive: a swarm service does not belong to a hive, and hive stays a property of which authenticated receiver accepted a push, not something a scrape can acquire.

Empty by default, in which case no scrape receiver, processor or pipeline is emitted at all — an enabled scraper with nothing to scrape is the inert configuration this option exists to avoid.

Type: attribute set of string

Default:

{ }

Example:

{ forgejo = "127.0.0.1:3000"; }

Declared by:

services.hyperhive.swarm.otel.telemetryPort

Port this collector serves its own metrics on — queue depth, refused and dropped samples, exporter failures. How you find out that telemetry is being lost, so it is worth keeping rather than switching off.

⚠️ Deliberately not 8889’s neighbour 8888, which is the collector’s built-in default and therefore what the hive tier already binds. Two collectors share a network namespace whenever they are co-located, and unlike the OTLP port this one appears nowhere in either config — it is a default inside the binary, so nothing that compares configured ports can see the clash. The second collector to start simply dies with bind: address already in use.

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

Default:

8889

Declared by:

services.hyperhive.swarm.snapshotStore.address

Mesh address of the swarm’s snapshot store — the single btrfs receive endpoint every hive in this swarm pushes agent snapshots to. Bare IP, no prefix.

There is exactly one store per swarm, not one per peer: the receiver keys destinations by agent, so an agent that migrates between hives keeps a single unbroken incremental chain. Per-hive stores would split that chain in two, which is the case the store exists to serve.

Null means this swarm has no store configured, and pushing fails saying so rather than guessing an address. Set it on every hive that pushes; the receiving host separately sets services.hyperhive.snapshotStore.enable.

Type: null or string

Default:

null

Example:

"10.100.0.1"

Declared by:

services.hyperhive.swarm.snapshotStore.port

TCP port the swarm’s snapshot store listens on. Must match the receiving host’s services.hyperhive.snapshotStore.port.

Defaulted (unlike address) because it is a shared convention both sides read from the same option docs — whereas an address is deployment-specific and cannot be guessed.

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

Default:

51821

Declared by:

services.hyperhive.swarm.statusPublish.tokenEndpoint

The swarm IdP’s OAuth2 token endpoint. This hive mints a client_credentials access token there and presents it when connecting to the queue, which authenticates it as hive-<hiveName> — the client nix/host-modules/swarm-authelia.nix already declares for every entry in services.hyperhive.swarm.hives.

Type: null or string

Default:

"${swarm.authelia.url}/api/oidc/token" when this host runs both the queue and the IdP, else null

Example:

"https://auth.example.com/api/oidc/token"

Declared by:

services.hyperhive.swarm.ui.package

Static build of the swarm UI. nginx serves this store path directly — there is no server-side component beyond the controller’s own API.

Wired by default from this flake’s own package set (see flake.nix), the same way swarm.controller.package is. There is deliberately no overlay in this project, so a pkgs.swarm-ui default here would name an attribute that does not exist on any real deployment.

Type: package

Default:

hyperhive.packages.${system}.swarm-ui

Declared by:

services.hyperhive.swarm.victorialogs.package

VictoriaLogs package to run.

Type: package

Default:

pkgs.victorialogs

Declared by:

services.hyperhive.swarm.victorialogs.domain

Name the gateway serves this on, behind the same authelia auth_request gate as the swarm UI’s own vhost. A sibling of the swarm’s other service names, so the swarm-services sub-CA can issue for it — see hive-tls.nix for why a service name being a sibling rather than a child decides which CA may sign it, and swarm.nix’s serviceDomains' for where this name has to be registered for that to actually happen.

Type: string

Default:

"logs.${services.hyperhive.swarm.domain}"

Declared by:

services.hyperhive.swarm.victorialogs.machine

Container name. Read-only: the name appears in host paths and in machinectl, so it is a fact other modules may read rather than a knob.

Type: string (read only)

Default:

"swarm-victorialogs"

Declared by:

services.hyperhive.swarm.victorialogs.port

Loopback port the store listens on. Upstream’s default, kept because there is no reason to move it and a familiar number is one less thing an operator has to look up.

⚠️ Every swarm container shares the host’s network namespace, so this is a swarm-wide claim rather than a per-container one — two modules picking the same number collide at runtime with no bind error and nothing in any log. state/eval-port-collisions.sh checks the class.

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

Default:

9428

Declared by:

services.hyperhive.swarm.victoriametrics.package

VictoriaMetrics package to run.

Type: package

Default:

pkgs.victoriametrics

Declared by:

services.hyperhive.swarm.victoriametrics.domain

Name the gateway serves this on. A sibling of the swarm’s other service names, so the swarm-services sub-CA can issue for it — see hive-tls.nix for why a service name being a sibling rather than a child decides which CA may sign it.

Type: string

Default:

"metrics.${services.hyperhive.swarm.domain}"

Declared by:

services.hyperhive.swarm.victoriametrics.machine

Container name. Read-only: the name appears in host paths and in machinectl, so it is a fact other modules may read rather than a knob.

Type: string (read only)

Default:

"swarm-victoriametrics"

Declared by:

services.hyperhive.swarm.victoriametrics.port

Port VictoriaMetrics listens on, bound to loopback only (see below). Upstream’s own default, kept so an operator reading VictoriaMetrics documentation finds what they expect.

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

Default:

8428

Declared by: