Skip to main content
Celum is a Next.js dashboard backed by a Go API. The backend owns authentication, evaluates IAM, talks to each supervisor’s Kubernetes API, and stores control-plane data in PostgreSQL and network-flow analytics in ClickHouse.

Components

Frontend

Next.js 16 (App Router) + React 19, TypeScript, Tailwind, shadcn/ui.

Backend

Go with the Chi router; a dynamic Kubernetes client per supervisor.

PostgreSQL

Control-plane state: supervisors, cluster templates, addons, IAM policies/ groups, audit logs, cached data.

ClickHouse

Network-flow analytics powering the security topology and forensics views.

Request pipeline

Every API request passes through an ordered middleware chain before reaching a handler:
1

Tracing + access log

An OpenTelemetry span (named after the route pattern) and a structured access log line.
2

Recoverer

Panics are caught and turned into 500s instead of crashing the process.
3

CORS

Cross-origin handling scoped to the configured frontend origin.
4

Session

Validates the k8s-gate-session cookie (or a Bearer API token) and puts the user’s identity and groups into the request context.
5

Audit

Mutating requests (POST/PUT/DELETE) are recorded with user, action, resource, status, and duration.
6

Authorization (IAM)

The route is resolved to an action + KRN and checked against the user’s policies. See Permissions model.
Result: a handler runs, or a 403 does — and either way the attempt is already recorded, because audit sits ahead of authorization in the chain. A denied request is in the audit log with the user who made it, which is what makes the log useful for “who tried” and not only “who succeeded”. The order is deliberate throughout: session precedes audit so the log has an identity to attribute to, and authorization is last so it can resolve the route into the same action and KRN vocabulary the log already recorded.

Two deliberate exceptions to that chain

With MCP_ENABLED=true the backend also serves /mcp, and the IAM middleware skips it. That is not a hole: each tool declares its own method and path and re-dispatches internally, so the work is authorized as the route it actually calls. The audit middleware skips /mcp for the same reason — the re-dispatched handler is what gets recorded, so a tool’s effect appears under the real route rather than under /mcp. The second exception is narrower and worth knowing if you ever wonder why a write is missing from the log: API-token minting is never audited, because the response body carries the plaintext secret and the audit log must not capture it.

Multi-supervisor model

Celum manages many supervisor clusters at once. Each supervisor is a kubeconfig — provided as a file (filename = supervisor name) or stored in the database. An empty supervisor name resolves to a synthesized __default__. Kubernetes clients are cached per supervisor, and every supervisor call is tagged with its name in traces. See Supervisors & clusters for the full model.

Provider detection

A cluster’s provider is read from its Cluster API spec.infrastructureRef.kind and mapped to a friendly name — for example: Unmapped kinds fall back to the kind name with the Cluster suffix stripped.

Data fetching

The frontend uses two helpers, never raw fetch: The client API base URL is injected at request time via a runtime-config script (window.__APP_CONFIG__), so the same build runs against any environment.

Observability

The backend emits OpenTelemetry traces over OTLP/HTTP when OTEL_EXPORTER_OTLP_ENDPOINT is set (a no-op tracer otherwise, so there’s no overhead when unconfigured). Spans cover HTTP handlers, per-supervisor Kubernetes calls, and PostgreSQL/ClickHouse queries, and structured logs carry the active trace ID for correlation.

Performance patterns

Read-only endpoints that hit a supervisor’s API use a stale-while-revalidate cache: fresh values are served immediately, stale values are returned while a refresh runs in the background, and a cold miss is bounded by a short timeout with an empty fallback — so one unreachable supervisor can’t hang a page. Helm release listings use a fast path that reads release Secrets directly, avoiding the Kubernetes API discovery cost.
A consequence worth knowing: a page can show data from a supervisor that has since become unreachable. Values are served stale while a refresh runs behind them, and a cold miss returns empty rather than an error.

Backend layout

The Go backend is organized under backend/internal/: Plus integration packages for DNS, IPAM, GitOps, and related services.
The secret store is fail-closed at boot. Setting SECRET_BACKEND tells Celum that credentials come from the store, so a store that is unreachable — or reachable and denying — stops startup rather than falling back to environment variables. That is deliberate: a silent fallback would boot the platform with empty credentials once the env vars are removed. SECRETS_OPTIONAL=true relaxes it for local development and end-to-end runs, where no store is present.If the backend will not start and the logs name the secret backend, this is the reason — and the fix is the store or the credential, not the application.

Supervisors & clusters

How supervisors are discovered, named, and cached.

Permissions model

How a request becomes an action + KRN, and how it is evaluated.