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, Windows tray, MSI, 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"
export DEBUG=true
python -m uvicorn backend.main:app --reload --port 8081

Use a real JWT secret in production; for local dev, DEBUG=true enables the dev-token endpoint.

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
  • 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-tray – Builds the Windows tray app (ncclient-tray.exe) with optional bundled Nebula. Depends on the CLI build; uploads ncclient-tray-windows-amd64.exe.
  3. build-msi – Runs only on tag pushes. Downloads the Windows CLI and tray artifacts, copies them into installer/windows/redist/, builds the MSI with WiX 5, and uploads NebulaCommander-windows-amd64.msi.
  4. upload-release – Runs only on tag pushes. Downloads all artifacts (CLI, tray, MSI), flattens them, generates SHA256SUMS.txt, and uploads everything to the GitHub Release for that tag. Release is not draft; files can be overwritten.
  5. checksums – Generates checksums for the Actions UI; release gets checksums from the upload-release job.
  6. 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 tray app, and the MSI, 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.


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 tray app, the Windows MSI, 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 tray app

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-tray.exe. The build can optionally bundle the Nebula Windows binary; see client/windows/README.md and build.py for details.


Windows MSI

The MSI installs the ncclient CLI and the tray app. You need both executables and WiX 5.

  1. Get the two 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/ncclient-tray.exe (from client/windows/dist/ncclient-tray.exe)
  2. Install WiX 5 – e.g. dotnet tool install --global wix --version 5.0.2. Add the Util extension once:

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

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

    Replace 0.1.12 with the version you are building.

Output: NebulaCommander-windows-amd64.msi.


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. When DEBUG=true or OIDC is not configured, returns a JWT (token, expires_in). Grants full admin access. Returns 404 in production when OIDC is configured.
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 authorization code for tokens and redirects to frontend with JWT in query (/auth/callback?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).
GET/api/auth/reauth/callbackNoReauth OAuth callback. Validates state (challenge) and redirects to frontend with reauth token.

Heartbeat (/api/nodes)

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

MethodPathAuthDescription
POST/api/nodes/{node_id}/heartbeatYes (JWT)Updates last_seen and sets node status to active. Call periodically from enrolled nodes. 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 list of node objects (id, network_id, hostname, ip_address, groups, is_lighthouse, is_relay, 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. 409 if removing the only lighthouse. Response: {"ok": true}.
DELETE/api/nodes/{node_id}YesDelete node: release IP, remove host cert/key files, delete related records. 204. 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; node record is kept. Releases IP and removes cert/key files. Node can re-enroll later. 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}.

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.

Users (/api/users)

System admin user management. All endpoints require system-admin role.

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.

Node requests (/api/node-requests)

Request/approve workflow for node creation (e.g. when auto-approve is off).

MethodPathAuthDescription
POST/api/node-requestsYesCreate a node request. Body: network_id, hostname, optional groups, is_lighthouse, is_relay. If user has manage_nodes or network has auto_approve_nodes, request is approved immediately and node is created. Response includes status, created_node_id if approved.
GET/api/node-requestsYesList node requests. Query: optional network_id, status. Users see own requests; network owners/admins see requests for their networks; system admins see all.
POST/api/node-requests/{request_id}/approveYesApprove a pending request; creates the node and allocates IP. Body: empty object. Requires manage_nodes on the network.
POST/api/node-requests/{request_id}/rejectYesReject a pending request. Body: reason. Requires manage_nodes on the network.

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, dev-token, logout, reauth
  • /api/nodes — Heartbeat, and full node CRUD + config/certs/revoke/re-enroll
  • /api/networks — Networks CRUD, group firewall, check-ip, and network users (permissions)
  • /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)
  • /api/users — User list/detail/update/delete (system admin)
  • /api/node-requests — Create/list/approve/reject node requests
  • /api/access-grants — Create/list/revoke temporary admin access
  • /api/invitations — Create/list/accept/resend/revoke invitations
  • /api/audit — List audit log (system admin)