Nodqora

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.

The Nodqora canvas: eleven nodes of a Kafka pipeline laid out left to right, each
                carrying a health glyph, with the plugin outcome chip in the bar above.
Eleven nodes and eleven edges of a pipeline running on a real cluster. Four plugins contributed to this canvas and no node names which.

Install

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 is not a ladder

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.

The API

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.

Status

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.

Editions

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.

Source

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.

Support

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.