This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Development

This section covers building Nebula Commander from source, using CI, and the backend API.

  • Setup – Run the backend and frontend locally (venv, uvicorn, npm run dev).
  • GitHub Actions – Workflows for ncclient binaries, releases, and Docker images.
  • Manual Builds – Build ncclient, the Windows app/service and MSI, Linux packages, and Docker images locally.
  • API – REST API base path, OpenAPI docs, and router summary.

1 - Development Setup

Run the backend and frontend locally for development.

Backend

From the nebula-commander repository root:

python -m venv .venv
source .venv/bin/activate   # or .venv\Scripts\activate on Windows
pip install -r backend/requirements.txt
export NEBULA_COMMANDER_DATABASE_URL="sqlite+aiosqlite:///./backend/db.sqlite"
export NEBULA_COMMANDER_CERT_STORE_PATH="./backend/certs"
# Required — the backend refuses to boot without it. Generate one:
# python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
export NEBULA_COMMANDER_ENCRYPTION_KEY="your-fernet-key-here"
export NEBULA_COMMANDER_DEBUG=true
python -m uvicorn backend.main:app --reload --port 8081

Use a real JWT secret in production; for local dev, NEBULA_COMMANDER_DEBUG=true enables the dev-token endpoint. Every backend setting uses the NEBULA_COMMANDER_ prefix — a bare DEBUG=true without it is silently ignored.

Frontend

In another terminal:

cd frontend && npm install && npm run dev

Open http://localhost:5173. When the backend is in debug mode, you can log in via the dev token (no OIDC required).

Configuration

Set at least:

  • NEBULA_COMMANDER_DATABASE_URL – SQLite path (e.g. sqlite+aiosqlite:///./backend/db.sqlite)
  • NEBULA_COMMANDER_CERT_STORE_PATH – Directory for CA and host certs
  • NEBULA_COMMANDER_ENCRYPTION_KEY – Required; the backend won’t start without it
  • NEBULA_COMMANDER_DEBUG=true – Enables dev token and hot reload

See Configuration: Environment for all options.

2 - GitHub Actions

Nebula Commander uses two GitHub Actions workflows: one to build ncclient binaries (and create releases), and one to build and push Docker images.

Build ncclient Binaries

Workflow file: .github/workflows/build-ncclient-binaries.yml

Triggers

  • Version tags – Push a tag matching v* (e.g. v0.1.5) to build all binaries and create a GitHub Release. This is the only trigger that produces a release; there is no trigger on push to main.
  • Pull requests – Runs when changes touch client/binaries/**, client/windows/**, client/ncclient.py, installer/windows/**, or the workflow file itself.
  • Manual – Use “Run workflow” in the Actions tab (workflow_dispatch).

Platforms built

PlatformArtifact name
Linux x86_64ncclient-linux-amd64
Linux ARM64ncclient-linux-arm64
Windows x86_64ncclient-windows-amd64.exe
macOS Intelncclient-macos-amd64
macOS ARM64 (Apple Silicon)ncclient-macos-arm64

Windows ARM64 is not built (GitHub has no Windows ARM64 runners). Linux ARM64 is built in Docker with QEMU on the host runner.

Jobs and outputs

  1. build – Matrix job: builds the ncclient CLI for each platform. Uploads one artifact per platform.
  2. build-windows-service – Builds the NebulaCommanderService background service with PyInstaller (client/windows/build.py). Uploads ncclient-service-windows-amd64.exe.
  3. build-windows-app – Builds the WinUI 3 app with dotnet publish -c Release -r win-x64 (.NET 10) as a self-contained single file. Uploads NebulaCommanderApp-windows-amd64.exe.
  4. build-deb / build-rpm – Tag pushes only. Package the build job’s ncclient-linux-amd64 binary plus the service and desktop app into three .deb and three .rpm packages (nebula-commander-client, -service, -desktop), plus an arm64 client package from ncclient-linux-arm64 (nebula-commander-client-arm64.deb, nebula-commander-client-aarch64.rpm).
  5. build-flatpak – Tag pushes only. Builds the desktop app against the GNOME 51 runtime and uploads org.beardedtek.NebulaCommander.flatpak.
  6. build-msi – Tag pushes only. Downloads the Windows CLI, app, and service artifacts, copies all three into installer/windows/redist/, builds the MSI with WiX 5 (Util + UI extensions), and uploads NebulaCommander-windows-amd64.msi. Any pre-release suffix is stripped from the MSI version, since MSI versions must be purely numeric.
  7. upload-release – Tag pushes only. Downloads all artifacts (CLI, Windows app/service/MSI, .deb, .rpm, Flatpak), flattens them, generates SHA256SUMS.txt, and uploads everything to the GitHub Release for that tag. Release is not draft; files can be overwritten.
  8. checksums – Generates checksums for the Actions UI; release gets checksums from the upload-release job.
  9. summary – Prints build status and notes that release binaries were uploaded when the run was tag-triggered.

Creating a release

  1. Bump version (e.g. in client/pyproject.toml or as appropriate).
  2. Tag and push:
    git tag v0.1.5
    git push origin v0.1.5
    
  3. The workflow builds all platforms, the Windows app and service, the MSI, and the Linux packages, then creates the release and attaches all binaries and SHA256SUMS.

Manual run

In GitHub: Actions → “Build ncclient Binaries” → “Run workflow”. Choose branch and run. Artifacts appear in the run; no release is created unless you ran from a tag.


Build Docker Images

Workflow file: .github/workflows/build-docker-images.yml

Triggers

  • After ncclient workflow – Runs when the “Build ncclient Binaries” workflow completes. It runs only if that workflow succeeded. Version is taken from the commit that triggered the binaries workflow: if that commit has a tag v*, that tag (without the v) is used; otherwise version is latest.
  • Manual – “Run workflow” in the Actions tab. Version is latest unless the run is triggered by the binaries workflow.

Images built and pushed

All images are pushed to GitHub Container Registry (ghcr.io):

ImagePlatforms
ghcr.io/nixrtr/nebula-commander-backendlinux/amd64, linux/arm64
ghcr.io/nixrtr/nebula-commander-frontendlinux/amd64, linux/arm64
ghcr.io/nixrtr/nebula-commander-keycloaklinux/amd64, linux/arm64

Each image is tagged with the version (e.g. 1.2.3) and latest. Build uses Docker Buildx, layer caching (GitHub Actions cache), and the Dockerfiles under docker/. The frontend image is built with DOWNLOAD_BINARIES=1 so it can serve ncclient binaries from the release.

Summary job

A final job prints the version and the full image names with that tag.


Publish Package Repository

File: .github/workflows/publish-package-repo.yml

Rebuilds the signed apt and rpm repository at pkgs.nebulacommander.com (GitHub Pages) that Linux users add to install and update the client, service, and desktop app. See Linux Desktop App.

Triggers

  • workflow_run – After “Build ncclient Binaries” completes successfully for a stable v* tag. Pre-release tags (containing -, e.g. v0.7.0-rc1) are skipped.
  • workflow_dispatch – Rebuild the repository at any time, e.g. after rotating the signing key.

What it does

  1. Downloads the .deb/.rpm assets of the newest 5 stable GitHub Releases that have Linux packages.
  2. Runs packaging/repo/build_repo.py: lays out an apt repository (deb/, one stable suite, amd64 and arm64) and an rpm repository (rpm/), signs InRelease/Release.gpg, signs each RPM and repomd.xml, and writes gpg.key, the .sources/.repo files, and an index page.
  3. Verifies the signatures and deploys the site to GitHub Pages.

The repository has no state of its own; every run regenerates it from the Releases.

Secrets

  • PACKAGE_SIGNING_KEY – The armored private key used for signing.
  • PACKAGE_SIGNING_KEY_PASSPHRASE – Its passphrase.

Testing workflows locally (act)

You can run the workflows locally with act. Only Linux jobs run in Docker; Windows and macOS jobs do not use real Windows/macOS runners in act. See the repository .github/README.md for setup, usage, and how to simulate a tag push. Release upload and secrets still require GitHub when running with act.

3 - Manual Builds

You can build all ncclient binaries, the Windows service and app, the Windows MSI, the Linux .deb/.rpm/Flatpak packages, and the Docker images locally without using GitHub Actions.

ncclient CLI (standalone binary)

The CLI is built with PyInstaller from client/binaries/. Python 3.11 is used in CI.

Same platform (Linux x86_64, Windows x86_64, macOS)

From the repository root:

pip install -r client/binaries/requirements.txt
pip install -r client/requirements.txt
cd client/binaries
python build.py

Output: client/binaries/dist/ncclient (or ncclient.exe on Windows). Use python build.py --clean to remove build artifacts; python build.py --test to build and run basic tests.

Linux ARM64

CI builds Linux ARM64 in a Docker container because the host runner is x86_64. Locally you can do the same:

docker run --rm --platform linux/arm64 \
  -v "$(pwd):/work" -w /work/client/binaries \
  python:3.11-slim \
  bash -c "
    apt-get update && apt-get install -y binutils &&
    pip install --upgrade pip &&
    pip install -r requirements.txt &&
    pip install -r ../requirements.txt &&
    python build.py
  "

The executable will be in client/binaries/dist/ncclient (arm64). Run this from the repo root so $(pwd) mounts the full tree.

Windows ARM64

On a Windows ARM64 machine (or with an ARM64 Python), install dependencies and run python build.py in client/binaries. To force PyInstaller to target ARM64 from an x64 host, set PYINSTALLER_TARGET_ARCH=arm64 in the environment when running build.py (CI does this for the Windows ARM64 matrix; GitHub does not provide Windows ARM64 runners, so this is for local use only).


Windows service

The service (NebulaCommanderService) is what actually polls for config/certs and runs Nebula, as LocalSystem. It is built with PyInstaller from client/windows/.

From the repository root:

pip install -r client/requirements.txt
pip install -r client/windows/requirements.txt
pip install pyinstaller
cd client/windows
python build.py

Output: client/windows/dist/ncclient-service.exe. The service does not bundle Nebula: it uses the copy the app downloads into %ProgramData%\nebula-commander\nebula\, the path set in Settings, or nebula.exe on PATH.

Note: ncclient-service.exe only does anything useful when registered as a real Windows Service (which the MSI does via WiX’s ServiceInstall/ServiceControl elements) - there is no standalone install/remove subcommand.

Windows app (WinUI 3)

The windowed app lives in client/windows-app/ and needs the .NET 10 SDK (CI uses 10.0.x). From that directory:

dotnet publish -c Release -r win-x64

Output: client/windows-app/bin/Release/net10.0-windows10.0.26100.0/win-x64/publish/NebulaCommanderApp.exe, a single self-contained file that bundles the .NET runtime and Windows App SDK. See client/windows-app/README.md for details.


Windows MSI

The MSI installs the ncclient CLI, the Windows app, and the service. You need all three executables and WiX 5.

  1. Get the three executables – Build as above or download from a release. Copy them into installer/windows/redist/:

    • redist/ncclient.exe (from client/binaries/dist/ncclient.exe)
    • redist/NebulaCommanderApp.exe (from the dotnet publish output above)
    • redist/ncclient-service.exe (from client/windows/dist/ncclient-service.exe)
  2. Install WiX 5 – e.g. dotnet tool install --global wix --version 5.0.2. Add the Util and UI extensions once:

    wix extension add -g WixToolset.Util.wixext/5.0.0
    wix extension add -g WixToolset.UI.wixext/5.0.2
    
  3. Build the MSI – From installer/windows/:

    wix build Product.wxs -ext WixToolset.Util.wixext -ext WixToolset.UI.wixext -o NebulaCommander-windows-amd64.msi -d Version=0.5.1 -arch x64
    

    Replace 0.5.1 with the version you are building. It must be purely numeric (major.minor.patch): strip any pre-release suffix such as -rc1. installer/windows/build-msi.ps1 wraps this and checks that all three files are in redist/ first.

Output: NebulaCommander-windows-amd64.msi. Installing it registers NebulaCommanderService (auto-start, LocalSystem) and grants Authenticated Users start/stop/query rights on it, so the app’s Start/Stop/Restart buttons work without a UAC prompt.


Linux packages (.deb / .rpm)

Both builders produce three packages: nebula-commander-client (the frozen CLI, arch-specific), nebula-commander-service (systemd unit, D-Bus policy, polkit action/rule; depends on the distro’s nebula package), and nebula-commander-desktop (the GTK4 app as plain Python source; arch: all). From the repository root:

# .deb - needs dpkg-deb (a Debian/Ubuntu host, container, or WSL)
python3 packaging/deb/build.py --version 0.5.1

# .rpm - needs rpmbuild (Fedora, or the `rpm` package on Debian/Ubuntu)
python3 packaging/rpm/build.py --version 0.5.1

The client package uses client/binaries/dist/ncclient, building it first if it’s missing; pass --ncclient-binary PATH to use a binary you already have (CI passes the release’s ncclient-linux-amd64). --only desktop service builds a subset, and --version defaults to git describe. Output goes to packaging/deb/dist/ and packaging/rpm/dist/.

Linux Flatpak

The Flatpak contains the desktop app only (it still needs nebula-commander-service on the host). It builds against the GNOME 51 runtime:

flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak install --user -y flathub org.gnome.Platform//51 org.gnome.Sdk//51
cd packaging/flatpak
python3 sync-sources.py   # vendor the client sources the manifest builds from
flatpak-builder --force-clean --user --repo=repo --install-deps-from=flathub \
  build-dir org.beardedtek.NebulaCommander.yaml
flatpak build-bundle repo org.beardedtek.NebulaCommander.flatpak org.beardedtek.NebulaCommander

Output: packaging/flatpak/org.beardedtek.NebulaCommander.flatpak.


Docker

Backend and frontend (compose)

From the repository root:

cd docker
docker compose build

This builds the backend and frontend images with default build-args. No Keycloak image is built by default; use the Keycloak Dockerfile separately if needed.

Backend image (docker build)

From the repository root:

docker build -f docker/backend/Dockerfile -t nebula-commander-backend:local .

Optional build-args: VERSION (default latest), NEBULA_VERSION (default 1.8.2).

Frontend image (docker build)

From the repository root:

docker build -f docker/frontend/Dockerfile -t nebula-commander-frontend:local .

Build-args:

  • VERSION – Version tag used when downloading ncclient binaries (default latest).
  • DOWNLOAD_BINARIES – Set to 1 to download ncclient binaries from GitHub releases (by version) into the image; set to 0 (default) for local builds that do not need bundled binaries.

Example with version and binaries:

docker build -f docker/frontend/Dockerfile \
  --build-arg VERSION=0.1.12 \
  --build-arg DOWNLOAD_BINARIES=1 \
  -t nebula-commander-frontend:0.1.12 .

Keycloak image

From the repository root:

docker build -f docker/keycloak/Dockerfile -t nebula-commander-keycloak:local .

No required build-args. For the nebula login background, ensure nebula-bg.webp exists under docker/keycloak-theme/nebula/login/resources/img/ (or copy from frontend/public/nebula-bg.webp) before building.

Multi-architecture (Buildx)

To build for linux/amd64 and linux/arm64 and push (e.g. to GHCR):

docker buildx build --platform linux/amd64,linux/arm64 \
  -f docker/backend/Dockerfile \
  -t ghcr.io/nixrtr/nebula-commander-backend:latest \
  --build-arg VERSION=latest \
  --push .

Use the same pattern for the frontend (with VERSION and DOWNLOAD_BINARIES as needed) and keycloak Dockerfiles.

4 - API

The Nebula Commander backend exposes a REST API under the base path /api. All routes are prefixed with /api. Most endpoints require a valid JWT in the Authorization: Bearer <token> header unless noted otherwise.

OpenAPI docs

When the backend is running in debug mode, interactive documentation is available at:

  • Swagger UI – /api/docs
  • ReDoc – /api/redoc

For full request/response schemas and parameters, use the interactive docs.


Health and root

MethodPathAuthDescription
GET/apiNoRoot response: name, version, status.
GET/api/healthNoHealth check. Returns {"status": "healthy"}.

Auth (/api/auth)

Authentication and session management. OIDC (e.g. Keycloak) is used when configured; otherwise a dev token is available in debug mode.

MethodPathAuthDescription
GET/api/auth/dev-tokenNoDevelopment only. Returns a JWT (token, expires_in) granting full admin access. Requires DEBUG=true (loopback requests only) or NEBULA_COMMANDER_STANDALONE_ADMIN_BOOTSTRAP=true when no OIDC provider is configured — it no longer falls back to enabled-by-default for standalone deployments. Returns 404 when OIDC is configured.
POST/api/auth/exchangeNoTrade a one-time exchange code (from the /auth/callback or reauth redirect) for the real JWT. Single-use, 60-second-lived. Response: {"token": "..."}. 400 if invalid, already used, or expired.
GET/api/auth/meOptionalCurrent user info. Returns {"authenticated": false} or {"authenticated": true, "sub", "email", "role", "system_role"}.
GET/api/auth/loginNoRedirects to the OIDC provider for login. Returns 501 if OIDC is not configured.
GET/api/auth/oidc-statusNoOIDC provider readiness. Returns {"status": "ok"}, {"status": "disabled"}, or 503 if provider is unavailable.
GET/api/auth/callbackNoOAuth callback. Exchanges the authorization code for tokens, then redirects to the frontend with a one-time exchange code, not the JWT itself (/auth/callback?code=...) — this keeps the token out of the URL, browser history, referrer headers, and access logs. The frontend immediately calls POST /api/auth/exchange to trade it for the real token.
GET/api/auth/logoutNoLogs out and redirects to OIDC logout (or frontend if OIDC not configured).
POST/api/auth/reauth/challengeYesCreates a reauthentication challenge for critical operations. Body: none. Response: challenge, reauth_url. Used before destructive actions (e.g. delete network/node, revoke certificate, delete user).
GET/api/auth/reauth/callbackNoReauth OAuth callback. Validates state (challenge) and redirects to the frontend with a one-time exchange code for the reauth token (same exchange pattern as login).

Heartbeat (/api/nodes)

Used by ncclient (or other clients) to report node liveness.

MethodPathAuthDescription
POST/api/nodes/{node_id}/heartbeatDevice tokenUpdates last_seen and sets node status to active. Call periodically from enrolled nodes using Authorization: Bearer <device_token> — not a human JWT. Body (all optional): interval_seconds (the client’s actual poll interval, clamped 10–3600s, used by the dashboard to detect an offline node relative to its real cadence), peer_reachability ({node_id: reachable} — only honored when the reporting node is itself a lighthouse, checked server-side and scoped to its own network, so a node can’t report on or spoof nodes it has no relationship to; see Device: lighthouse-peers below). Response: {"ok": true, "last_seen": "<iso>"}.

Networks (/api/networks)

Create and manage Nebula networks. Permissions are enforced per network (owner, member, and capability flags).

MethodPathAuthDescription
GET/api/networksYesList networks the user can access. Includes role, can_manage_nodes, can_invite_users, can_manage_firewall per network. System admins see all networks (with limited data).
POST/api/networksYesCreate a network. Body: name, subnet_cidr. Creator becomes owner. Returns full network object. 409 if name exists.
GET/api/networks/{network_id}YesGet a single network. System admins need an access grant to see CA path.
PATCH/api/networks/{network_id}YesUpdate network (owner only). Body: optional fields (currently no network-level firewall; use group firewall).
DELETE/api/networks/{network_id}YesDelete network (owner only; system admins can delete any). Body: reauth_token, confirmation (must match network name). 204 on success.
GET/api/networks/{network_id}/group-firewallYesList per-group firewall configs. Requires can_manage_firewall. Response: list of {group_name, inbound_rules}.
PUT/api/networks/{network_id}/group-firewall/{group_name}YesCreate or update inbound firewall rules for a group. Body: inbound_rules (each: allowed_group, protocol (any/tcp/udp/icmp), port_range, optional description).
DELETE/api/networks/{network_id}/group-firewall/{group_name}YesRemove group firewall config for that group. 204 on success.
GET/api/networks/{network_id}/check-ipYesCheck if an IP is available in the network. Query: ip. Response: {"available": true or false}. 400 if IP not in subnet.

Nodes (/api/nodes)

Manage Nebula nodes (hosts) within networks. Used by the Web UI and for manual cert/config workflows.

MethodPathAuthDescription
GET/api/nodesYesList nodes. Query: optional network_id. Returns only nodes the user can access (networks they have permission for, or node-level access grants). Response: list of node objects (id, network_id, hostname, ip_address, groups, is_lighthouse, is_relay, platform, status, etc.).
GET/api/nodes/{node_id}YesGet a single node by ID.
PATCH/api/nodes/{node_id}YesUpdate node. Body (all optional): group, is_lighthouse, is_relay, public_endpoint, lighthouse_options, logging_options, punchy_options, platform (desktop/ios/android — 400 if setting a non-desktop platform while the node is a lighthouse or relay). 409 if removing the only lighthouse. A group change automatically re-signs the node’s certificate in place (same IP and keypair — a Nebula cert bakes its group in at signing time) when the node already has one. Response: {"ok": true, "cert_resigned": true or false}.
DELETE/api/nodes/{node_id}YesDelete node: release IP, remove host cert/key files, delete related records. Body: reauth_token, confirmation (must match node hostname). 204 on success. 409 if node is the only lighthouse.
GET/api/nodes/{node_id}/configYesGenerate and return Nebula YAML config for the node (with inline PKI when key is stored). Response: application/yaml attachment.
GET/api/nodes/{node_id}/certsYesReturn a ZIP with ca.crt, host.crt, optional host.key, and README.txt.
POST/api/nodes/{node_id}/revoke-certificateYesRevoke the node’s certificate and take it offline; node record is kept and can re-enroll later. Body: reauth_token, confirmation (must match node hostname). Releases IP and removes cert/key files. Response: {"ok": true}.
POST/api/nodes/{node_id}/re-enrollYesRevoke existing cert (if any) and issue a new one for this node. Frontend typically creates an enrollment code afterward. Response: {"ok": true, "node_id": id}.
PUT/api/nodes/{node_id}/subnet-routerYesConsumer-side pick of which other node’s advertised subnets this node should route through. Body: router_node_id (int or null to clear). Atomically adds this node to every matching route’s consumers on the chosen gateway and removes it from every other gateway’s matching routes — a node uses at most one subnet router at a time. 400 if the target routes through itself, isn’t on the same network, or doesn’t advertise a subnet. See Subnet Routers and Exit Nodes.
PUT/api/nodes/{node_id}/exit-nodeYesSame as above, for the exit-route pair (0.0.0.0/0 + ::/0) instead of subnet routes. Body: exit_node_id.

Certificates (/api/certificates)

Create or sign host certificates. Used when creating nodes from the Web UI or when using client-generated keys (e.g. betterkeys).

MethodPathAuthDescription
POST/api/certificates/signYesSign a host certificate (client sends public key). Body: network_id, name, public_key, optional group, suggested_ip, duration_days. Response: ip_address, certificate (PEM), optional ca_certificate. Creates or updates node record.
POST/api/certificates/createYesCreate a host certificate (server generates keypair). Body: network_id, name, optional group, suggested_ip, duration_days, is_lighthouse, is_relay, public_endpoint, lighthouse_options, punchy_options. Response: node_id, hostname, ip_address, certificate, private_key, optional ca_certificate. First node in network must be lighthouse. 409 if node name exists or suggested IP is taken.
GET/api/certificatesYesList issued certificates. Query: optional network_id. Response: list of {id, node_id, node_name, network_id, network_name, ip_address, issued_at, expires_at, revoked_at}.

Device (/api/device)

Used by ncclient for enrollment and for fetching config/certs with a device token. Flow: create enrollment code (admin) → device redeems code at POST /enroll → device uses returned token for GET /config and GET /certs.

MethodPathAuthDescription
POST/api/device/enrollment-codesYes (JWT)Create a one-time enrollment code for a node. Body: node_id, expires_in_hours (default 24). Response: code, expires_at, node_id, hostname. Node must already have a certificate.
POST/api/device/enrollNoPublic. Redeem a one-time code. Body: code. Response: device_token, node_id, hostname. Rate limited (e.g. 5 attempts per 15 min per IP). 404 if code invalid/expired, 400 if already used or expired.
GET/api/device/configDevice tokenReturn Nebula YAML config for the device (inline PKI). Header: Authorization: Bearer <device_token>. Optional If-None-Match: <etag> for 304 when unchanged.
GET/api/device/certsDevice tokenReturn ZIP with ca.crt, host.crt, optional host.key, README.txt for the device.
GET/api/device/dns-client-configDevice tokenSplit-horizon DNS config for the device: domain and dns_servers (lighthouse Nebula IPs). 404 if DNS is not enabled for the network. Used by ncclient with --accept-dns.
GET/api/device/lighthouse-peersDevice tokenReturn the other nodes on this device’s network (node_id, ip_address, hostname), for a lighthouse to ping and report reachability on via its heartbeat’s peer_reachability. Non-lighthouse devices always get an empty list — keeps client logic trivial and avoids leaking network topology to devices that don’t need it.
GET/api/device/dnsmasq.confDevice tokendnsmasq zone config for this device’s network (for lighthouse/container clients), derived from the token. Returns text/plain. Optional If-None-Match: <etag> for 304. Rate limited per device.

Networks DNS (/api/networks/{network_id}/dns)

Per-network DNS configuration for split-horizon DNS (domain and aliases). Network owners only.

MethodPathAuthDescription
GET/api/networks/{network_id}/dnsYesGet DNS config: domain, enabled, upstream_servers. Default domain is network name when not set.
PUT/api/networks/{network_id}/dnsYesUpdate DNS config. Body: optional domain, enabled, upstream_servers.
GET/api/networks/{network_id}/dns/aliasesYesList DNS aliases (hostname → node). Response: list of {id, alias, node_id, node_hostname}.
POST/api/networks/{network_id}/dns/aliasesYesCreate alias. Body: alias (hostname label), node_id. 409 if alias exists.
DELETE/api/networks/{network_id}/dns/aliases/{alias_id}YesDelete alias. 204.
GET/api/networks/{network_id}/dns/dnsmasq.confDevice tokenDevice-facing dnsmasq zone config, network given explicitly in the path rather than derived from the token. 403 if the token’s node isn’t a member of network_id. Otherwise identical to /api/device/dnsmasq.conf above (same ETag/304 behavior).

Users: self-service (/api/users/me)

Per-account preferences, available to any authenticated user (not just admins). See Appearance.

MethodPathAuthDescription
GET/api/users/me/themeYesGet the current user’s active color theme: their saved overrides merged over the built-in defaults, so the response is always a complete token set.
PUT/api/users/me/themeYesMerge the given tokens into the current user’s active theme (partial update — omitted keys are left as-is). Body: theme (map of token key → {light, dark} hex pair). Each key must be a known token; each color must match ^#[0-9a-fA-F]{6}$. 400 on an unknown key or invalid color.
GET/api/users/me/themesYesList the current user’s saved theme presets. Response: list of {id, name, tokens, created_at} — full token sets included, not just names.
POST/api/users/me/themesYesSave a named preset. Body: name, tokens (same shape as PUT /me/theme’s theme). 400 if the name is empty, over 100 characters, or already used by one of this user’s other saved themes. There’s no separate “apply” endpoint — applying a saved theme is just PUT /me/theme with its tokens.
DELETE/api/users/me/themes/{theme_id}YesDelete a saved theme. Scoped to the requesting user — 404 (not 403) if the theme belongs to someone else. 204 on success.

Users (/api/users)

System admin user management. All endpoints below require system-admin role (self-service /me endpoints above do not).

MethodPathAuthDescription
GET/api/usersSystem adminList all users. Response: list of {id, oidc_sub, email, system_role, created_at, network_count}.
GET/api/users/{user_id}System adminGet user details including networks (list of network id, name, role, permission flags).
PATCH/api/users/{user_id}System adminUpdate user. Body: optional system_role (system-admin or user).
DELETE/api/users/{user_id}System adminDelete user; removes all their network permissions. 204.

Access grants (/api/access-grants)

Temporary access for system admins to a specific network or node (e.g. for support). Only network owners can create grants.

MethodPathAuthDescription
POST/api/access-grantsYesCreate an access grant. Body: admin_user_id, resource_type (network or node), resource_id, duration_hours, reason. Target user must be system-admin.
GET/api/access-grantsYesList grants. Query: active_only (default true). Network owners see grants they created; system admins see grants for them.
DELETE/api/access-grants/{grant_id}YesRevoke a grant. Only the user who created the grant can revoke. 204.

Invitations (/api/invitations)

Invite users to join a network by email. Requires can_invite_users (or owner) on the network.

MethodPathAuthDescription
POST/api/invitationsYesCreate invitation. Body: email, network_id, role (owner/member), can_manage_nodes, can_invite_users, can_manage_firewall, expires_in_days (default 7). Sends email if SMTP configured. 400 if user already member or pending invitation exists.
GET/api/invitationsYesList invitations. Query: optional network_id, status_filter. Owners see invitations for their networks; system admins see all.
GET/api/invitations/public/{token}NoPublic. Get invitation details by token (no auth). Returns network name, inviter, role, permissions, status, expires_at. 410 if expired.
POST/api/invitations/{token}/acceptYesAccept invitation; creates NetworkPermission for current user. Must be logged in. Email should match invitation (optional check).
POST/api/invitations/{invitation_id}/resendYesResend invitation email. Pending only.
DELETE/api/invitations/{invitation_id}YesRevoke invitation (inviter or network owner). Sets status to revoked. 204.

Network permissions (/api/networks)

Manage which users have access to a network and their roles/permissions. Owner-only for list/update/remove; can_invite_users for add.

MethodPathAuthDescription
GET/api/networks/{network_id}/usersYesList users with access to the network. Owner or system admin. Response: list of user_id, email, role, can_manage_, invited_by_, created_at.
POST/api/networks/{network_id}/usersYesAdd user to network. Body: user_id, role (owner/member), can_manage_nodes, can_invite_users, can_manage_firewall. Requires can_invite_users. 400 if already member.
PATCH/api/networks/{network_id}/users/{target_user_id}YesUpdate user’s permissions. Body: optional role, can_manage_nodes, can_invite_users, can_manage_firewall. Owner only. Cannot demote the last owner.
DELETE/api/networks/{network_id}/users/{target_user_id}YesRemove user from network. Owner only. Cannot remove the last owner. 204.

Audit (/api/audit)

Read-only audit log. System admins only.

MethodPathAuthDescription
GET/api/auditSystem adminList audit log entries. Query: limit (default 50, max 200), offset, optional action, resource_type, from_date, to_date. Ordered by occurred_at descending. Response: list of id, occurred_at, action, actor_*, resource_type, resource_id, result, details, client_ip.

Summary

  • /api — Root, health
  • /api/auth — Login, callback, exchange, dev-token, logout, reauth
  • /api/nodes — Heartbeat (device token, carries peer_reachability), full node CRUD + config/certs/revoke/re-enroll, and subnet-router/exit-node consumer selection
  • /api/networks — Networks CRUD, group firewall, check-ip, network users (permissions), and per-network DNS (config, aliases, device-facing dnsmasq.conf)
  • /api/certificates — Sign host cert (client key), create host cert (server key), list certs
  • /api/device — Enrollment codes, enroll (public), config and certs (device token), lighthouse-peers, dns-client-config and dnsmasq.conf for split-horizon DNS
  • /api/users/me — Self-service: active color theme (get/set) and saved theme presets (list/create/delete), any authenticated user
  • /api/users — User list/detail/update/delete (system admin)
  • /api/access-grants — Create/list/revoke temporary admin access
  • /api/invitations — Create/list/accept/resend/revoke invitations
  • /api/audit — List audit log (system admin)