Architecture
Big picture
Headlamp has two halves that ship as one product. A Go backend serves the single-page frontend, holds the kubeconfig contexts, and reverse-proxies every Kubernetes API call from the browser to the right cluster with a bearer token attached. A React frontend renders the UI and never contacts a cluster directly; it always goes through the backend. The same backend and frontend also run inside an Electron shell as a desktop app. A plugin system loads extra JavaScript into the frontend at runtime so third parties can add pages without forking.
Components
Go backend server
backend/cmd/ is the HTTP server, built on gorilla/mux. Its entry point is main (backend/cmd/server.go:47), which calls StartHeadlampServer (backend/cmd/headlamp.go:1374). It owns six jobs: serving the frontend as a static single-page app, reverse-proxying to each cluster's Kubernetes API, authentication (OIDC and token cookies), plugin delivery, helper APIs (Helm, port-forward, drain), and a WebSocket multiplexer. The router registration lives in headlamp.go, a large file that wires every route.
React frontend
frontend/src/ is the TypeScript and React app (MUI for components, react-router v5 for routing). It lists, inspects, and edits Kubernetes resources. API access lives in frontend/src/lib/k8s/, split into a v1 layer (api/v1/clusterRequests.ts) and a newer v2 layer (api/v2/fetch.ts with React Query hooks). Both send their requests to the backend rather than to a cluster.
Plugin system
frontend/src/plugin/ in the frontend and plugins/headlamp-plugin for the SDK make the UI extensible at runtime. The backend lists installed plugins over an HTTP endpoint and serves each plugin's files statically; the frontend fetches each plugin's main.js, executes it, and lets it register into the UI through a Registry object.
Desktop app
app/ wraps the same backend and frontend in Electron to ship a Linux, macOS, and Windows desktop application from the same code.
How a request flows
Trace a resource fetch from a click in the browser to the kube-apiserver and back.
- The frontend calls
request(path)(frontend/src/lib/k8s/api/v1/clusterRequests.ts:95), which callsclusterRequest(clusterRequests.ts:123). It buildsfullPath = /clusters/{cluster}/{path}with the current cluster name (clusterRequests.ts:155, usingCLUSTERS_PREFIX) and sendsfetch(url, { credentials: 'include' })(clusterRequests.ts:165). Thecredentials: 'include'option carries the token cookie along with the request. - The backend router matches
PathPrefix("/clusters/{clusterName}/{api:.*}")(backend/cmd/headlamp.go:1884, registered byhandleClusterAPI). If caching is enabled,CacheMiddleWarewraps the handler. clusterRequestHandler(headlamp.go:1772) resolves the kubeconfig context key withgetContextKeyForRequest(r)and looks it up withc.KubeConfigStore.GetContext(contextKey)to get a*kubeconfig.Context(headlamp.go:1788,headlamp.go:1794).- It builds the destination URL:
url.Parse(kContext.Cluster.Server)(headlamp.go:1805), then rewrites the incoming request to the cluster by settingr.URL.Host,r.URL.Scheme, andr.URL.Path = mux.Vars(r)["api"], which strips the/clusters/{name}/prefix back off (headlamp.go:1828-1830). - It injects auth:
auth.GetTokenFromCookie(r, clusterName)reads the token and setsAuthorization: Bearer <token>(headlamp.go:1845,headlamp.go:1849). - It hands off with
kContext.ProxyRequest(w, r)(headlamp.go:1857), which lives inbackend/pkg/kubeconfig/kubeconfig.go:387. On first useSetupProxy(kubeconfig.go:431) builds anhttputil.NewSingleHostReverseProxy(URL)(kubeconfig.go:437) and wraps its transport in auserAgentRoundTripperthat stamps a Headlamp User-Agent (kubeconfig.go:34-51). After thatc.proxy.ServeHTTPforwards to the kube-apiserver (kubeconfig.go:395). - The response returns through the proxy to the frontend. When caching is on,
k8cachestores GET responses and invalidates on non-GET requests (server.go:285).
Key design decisions
Everything goes through the backend. The frontend cannot reach a kube-apiserver on its own: the token lives in a server-side cookie, and the proxy moves it into the Authorization header. This is what removes CORS from the picture, keeps the token out of browser-visible JavaScript, and lets a single instance front many clusters at once.
The reverse proxy is cached per context. Each kubeconfig.Context holds a lazily created proxy *httputil.ReverseProxy field (kubeconfig.go:71), so a cluster's proxy is built once and reused across requests rather than rebuilt each call.
Authorization stays with the cluster. Headlamp is a pass-through proxy; the kube-apiserver makes the allow-or-deny decision. The frontend uses access-review checks to hide UI it knows the user cannot use, but the real enforcement happens at the API server, so a hidden button is a courtesy and not the security boundary.
Extension points
- Frontend plugins: a plugin is JavaScript the frontend fetches and executes at runtime. It registers into the UI through a
Registryobject, adding sidebar entries (registerSidebarEntry,frontend/src/plugin/registry.tsx:301), routes (registerRoute,registry.tsx:445), detail-view sections (registerDetailsViewSection,registry.tsx:606), app-bar actions, and object-glance cards. The backend delivers the plugin files; the frontend runs them. - Stateless mode:
backend/cmd/stateless.goaccepts a kubeconfig in a request header instead of storing it server-side. The frontend setsopts.headers['KUBECONFIG']when it has one (clusterRequests.ts:151), which suits multi-tenant hosting where the server holds no cluster credentials. - Cluster inventory:
backend/pkg/clusterinventorydiscovers clusters dynamically fromClusterProfileresources rather than a static kubeconfig. - Helm, port-forward, and telemetry:
backend/pkg/helm,backend/pkg/portforward, andbackend/pkg/telemetry(OpenTelemetry) add helper APIs beyond the raw proxy.