A topology and health canvas for event-driven systems.
Four discovery plugins — yaml, kubernetes, kafka,
connect — fold into one graph under a configured precedence, three of them also
reporting health on their own cadence. You declare the edges nothing can observe; Nodqora
observes the rest and joins the two.
It runs on your own infrastructure, reads only, and is Apache-2.0.
Docker with Compose v2, and nothing else — no JDK and no clone.
curl -LO https://github.com/nodqora/nodqora/releases/latest/download/compose.yaml
mkdir -p config topology
docker compose up -d
Then open http://localhost:8080, which will tell you that no identity provider is
configured. That is a working install serving nothing: until nodqora.authentication
is declared, the API answers every request with a 401. The image also ships zero environments on
purpose, because a baked-in one could be overridden by your config but never removed. Both are
declared in ./config/application.yaml, and then a restart:
nodqora:
authentication:
oidc:
base-url: http://localhost:8080
issuer: https://idp.example.com/realms/acme
client-id: nodqora
environments:
homelab:
display-name: Homelab
plugins:
kubernetes:
namespaces: [n8n, monitoring, actual]
Nodqora signs people in with OIDC against your own identity provider — anything
that publishes a discovery document — and holds no accounts of its own. Everyone who signs in sees
every environment in the install; there are no roles to assign. An install that nobody else can
reach says authentication: none instead, which is how every release before 0.3.0
behaved. What each line means, what the provider has to be told, and where a client secret goes are
in install.md.
The container cannot see ~/.kube/config, so kubectl and k9s working
on the same machine do not mean Nodqora can list anything. Unless Nodqora runs inside the cluster
it observes, every namespace fails with Operation: [list] for kind: [Deployment] …
failed until it has a kubeconfig of its own. That is three changes and a recreate:
# 1. A self-contained kubeconfig beside compose.yaml (--flatten inlines the certificates)
kubectl config view --minify --flatten > kubeconfig
# then edit its server: line. 127.0.0.1 inside the container is the container, so use
# the cluster's LAN address, e.g. https://192.168.1.50:6443, or
# https://host.docker.internal:6443 for a cluster on this machine under Docker Desktop.
# 2. compose.yaml, under the nodqora service's volumes:
- ./kubeconfig:/etc/nodqora/kubeconfig:ro
# 3. config/application.yaml, beside namespaces:
kubeconfig: '\${file:/etc/nodqora/kubeconfig}'
# The backslash and single quotes are required: without them Spring expands the
# reference itself and the plugin receives the path, not the file (nodqora#111).
docker compose up -d # not restart: a new mount needs a new container
The address must also be one the API server's certificate names — k3s covers the node's own
addresses and takes --tls-san for any other. The file is a credential; it stays out
of any repository. Details are in
install.md.
That gives a canvas of workloads, each on its own row and none connected. That is correct:
Kubernetes says what runs, not what talks to what, so the kubernetes plugin emits no
edges. Edges come from a topology file you write — or from the kafka and
connect plugins, which observe real topics and connectors. Turn on the
yaml plugin beside kubernetes:
plugins:
yaml: { dir: /etc/nodqora/topology/homelab }
and write the edges into ./topology/homelab/, which compose.yaml
already mounts:
# ./topology/homelab/homelab.yaml
environment: homelab
nodes:
- key: cloudflared # the exact Deployment / StatefulSet name
calls: [n8n]
- key: n8n
calls: [postgres]
- key: grafana
queries: [prometheus] # written from the consumer's side; the arrow still follows the data
A key that does not exactly match a workload's name is not an error: it renders as
a second, unconnected card. DaemonSets never become nodes, so nothing can point at one. Then
docker compose restart nodqora. The verbs, owners and the annotations that carry the
same facts on your manifests are in
install.md.
The full route — upgrades and the backup they require, pointing at a PostgreSQL you already run, what the image bundles, troubleshooting — is in docs/install.md.
Health carries five values, and only three of them are ordered.
HEALTHY < DEGRADED < UNHEALTHY is a severity order.
UNKNOWN is an abstention and DISABLED is judgement suspended; neither
sits anywhere on that scale, and neither is averaged into a parent.
Alongside health, every plugin reports an outcome. Health says what we found; outcome says how well we looked. A failed poll leaves the last reading standing and says so, rather than turning a node green or red on evidence nobody gathered.
The same rule holds at the edges of the map. A node no plugin claimed is grey and labelled as such, not absent. An environment whose plugins are all misconfigured renders the declared topology with banners saying nothing observed it — which is the honesty layer working, not a broken install.
Three GETs, unversioned, unenveloped, read-only, and behind the same sign-in as the canvas.
GET /api/meta
GET /api/environments/{envKey}/graph
GET /api/environments/{envKey}/state
Nodqora observes; it does not serve traffic. Nothing in your estate stops when Nodqora stops, and nothing it writes reaches your clusters, brokers or connectors.
v0.3.0 is the current release. All five implementation slices are merged: the four plugins, the fold, health end-to-end and the honesty layer. The reference pipeline renders at 10 nodes and 9 edges in production, 7 and 6 in staging, and the build asserts those numbers against the document that specifies them.
Two releases have followed the first. 0.2.0 added a fifth plugin, Prometheus,
which carries a series onto a node as a figure and never votes on its health. 0.3.0
added sign-in: OIDC against your own identity provider, sessions in Postgres, and an API that
answers an unauthenticated request with a 401. Upgrading to it needs one new key in
application.yaml — the
release notes say which.
It is young. No test in the suite reaches a real cluster, broker or Connect worker — every plugin's outbound seam is exercised against a recording — so "the client cannot start" is a class of defect only running against real infrastructure will find. Defects of exactly that kind have been found and fixed by running it against a live pipeline.
There is one edition: Community, Apache-2.0. Nothing is for sale, and nothing on this site can be bought.
Nodqora is built open core, so a paid edition is intended eventually. It does not exist, and the line between the two is not being advertised while there is nothing on the other side of it. Two commitments hold regardless: no capability shipped in Community ever becomes paid, and the Community edition stays Apache-2.0 and self-hostable.
Where that line is drawn, and why each capability falls where it does, is recorded in the repository — docs/editions.md — along with the roadmap status that says how much of it is built.
Apache-2.0. Source, decisions and issue tracker: github.com/nodqora/nodqora. Every design decision is an ADR in the repository, and the domain model they describe is in CONTEXT.md.
Contributions take a DCO sign-off and there is no CLA.
For the Apache-2.0 edition, open an issue. That is the whole channel: support is public, so there is no support email and no private channel, and every answer is where the next person will find it. Nothing is promised about how fast — in practice every issue gets read.
That is the only support channel there is. There is no paid edition, so there is no paid support to buy alongside it.