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 certsDEBUG=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).
| Platform | Artifact name |
|---|
| Linux x86_64 | ncclient-linux-amd64 |
| Linux ARM64 | ncclient-linux-arm64 |
| Windows x86_64 | ncclient-windows-amd64.exe |
| macOS Intel | ncclient-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
- build – Matrix job: builds the ncclient CLI for each platform. Uploads one artifact per platform.
- 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. - 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. - 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. - checksums – Generates checksums for the Actions UI; release gets checksums from the upload-release job.
- summary – Prints build status and notes that release binaries were uploaded when the run was tag-triggered.
Creating a release
- Bump version (e.g. in
client/pyproject.toml or as appropriate). - Tag and push:
git tag v0.1.5
git push origin v0.1.5
- 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):
| Image | Platforms |
|---|
ghcr.io/nixrtr/nebula-commander-backend | linux/amd64, linux/arm64 |
ghcr.io/nixrtr/nebula-commander-frontend | linux/amd64, linux/arm64 |
ghcr.io/nixrtr/nebula-commander-keycloak | linux/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.
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.
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)
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
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
| Method | Path | Auth | Description |
|---|
GET | /api | No | Root response: name, version, status. |
GET | /api/health | No | Health 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.
| Method | Path | Auth | Description |
|---|
GET | /api/auth/dev-token | No | Development 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/me | Optional | Current user info. Returns {"authenticated": false} or {"authenticated": true, "sub", "email", "role", "system_role"}. |
GET | /api/auth/login | No | Redirects to the OIDC provider for login. Returns 501 if OIDC is not configured. |
GET | /api/auth/oidc-status | No | OIDC provider readiness. Returns {"status": "ok"}, {"status": "disabled"}, or 503 if provider is unavailable. |
GET | /api/auth/callback | No | OAuth callback. Exchanges authorization code for tokens and redirects to frontend with JWT in query (/auth/callback?token=...). |
GET | /api/auth/logout | No | Logs out and redirects to OIDC logout (or frontend if OIDC not configured). |
POST | /api/auth/reauth/challenge | Yes | Creates a reauthentication challenge for critical operations. Body: none. Response: challenge, reauth_url. Used before destructive actions (e.g. delete network). |
GET | /api/auth/reauth/callback | No | Reauth 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.
| Method | Path | Auth | Description |
|---|
POST | /api/nodes/{node_id}/heartbeat | Yes (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).
| Method | Path | Auth | Description |
|---|
GET | /api/networks | Yes | List 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/networks | Yes | Create a network. Body: name, subnet_cidr. Creator becomes owner. Returns full network object. 409 if name exists. |
GET | /api/networks/{network_id} | Yes | Get a single network. System admins need an access grant to see CA path. |
PATCH | /api/networks/{network_id} | Yes | Update network (owner only). Body: optional fields (currently no network-level firewall; use group firewall). |
DELETE | /api/networks/{network_id} | Yes | Delete 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-firewall | Yes | List per-group firewall configs. Requires can_manage_firewall. Response: list of {group_name, inbound_rules}. |
PUT | /api/networks/{network_id}/group-firewall/{group_name} | Yes | Create 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} | Yes | Remove group firewall config for that group. 204 on success. |
GET | /api/networks/{network_id}/check-ip | Yes | Check 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.
| Method | Path | Auth | Description |
|---|
GET | /api/nodes | Yes | List 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} | Yes | Get a single node by ID. |
PATCH | /api/nodes/{node_id} | Yes | Update 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} | Yes | Delete node: release IP, remove host cert/key files, delete related records. 204. 409 if node is the only lighthouse. |
GET | /api/nodes/{node_id}/config | Yes | Generate and return Nebula YAML config for the node (with inline PKI when key is stored). Response: application/yaml attachment. |
GET | /api/nodes/{node_id}/certs | Yes | Return a ZIP with ca.crt, host.crt, optional host.key, and README.txt. |
POST | /api/nodes/{node_id}/revoke-certificate | Yes | Revoke 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-enroll | Yes | Revoke 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).
| Method | Path | Auth | Description |
|---|
POST | /api/certificates/sign | Yes | Sign 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/create | Yes | Create 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/certificates | Yes | List 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.
| Method | Path | Auth | Description |
|---|
POST | /api/device/enrollment-codes | Yes (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/enroll | No | Public. 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/config | Device token | Return 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/certs | Device token | Return 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.
| Method | Path | Auth | Description |
|---|
GET | /api/users | System admin | List all users. Response: list of {id, oidc_sub, email, system_role, created_at, network_count}. |
GET | /api/users/{user_id} | System admin | Get user details including networks (list of network id, name, role, permission flags). |
PATCH | /api/users/{user_id} | System admin | Update user. Body: optional system_role (system-admin or user). |
DELETE | /api/users/{user_id} | System admin | Delete 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).
| Method | Path | Auth | Description |
|---|
POST | /api/node-requests | Yes | Create 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-requests | Yes | List 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}/approve | Yes | Approve a pending request; creates the node and allocates IP. Body: empty object. Requires manage_nodes on the network. |
POST | /api/node-requests/{request_id}/reject | Yes | Reject 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.
| Method | Path | Auth | Description |
|---|
POST | /api/access-grants | Yes | Create 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-grants | Yes | List grants. Query: active_only (default true). Network owners see grants they created; system admins see grants for them. |
DELETE | /api/access-grants/{grant_id} | Yes | Revoke 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.
| Method | Path | Auth | Description |
|---|
POST | /api/invitations | Yes | Create 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/invitations | Yes | List invitations. Query: optional network_id, status_filter. Owners see invitations for their networks; system admins see all. |
GET | /api/invitations/public/{token} | No | Public. Get invitation details by token (no auth). Returns network name, inviter, role, permissions, status, expires_at. 410 if expired. |
POST | /api/invitations/{token}/accept | Yes | Accept invitation; creates NetworkPermission for current user. Must be logged in. Email should match invitation (optional check). |
POST | /api/invitations/{invitation_id}/resend | Yes | Resend invitation email. Pending only. |
DELETE | /api/invitations/{invitation_id} | Yes | Revoke 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.
| Method | Path | Auth | Description |
|---|
GET | /api/networks/{network_id}/users | Yes | List 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}/users | Yes | Add 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} | Yes | Update 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} | Yes | Remove user from network. Owner only. Cannot remove the last owner. 204. |
Audit (/api/audit)
Read-only audit log. System admins only.
| Method | Path | Auth | Description |
|---|
GET | /api/audit | System admin | List 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)