Architecture Overview

Podscape is a three-process Electron application. Each process has a strictly defined responsibility.

Process Layout

Renderer (React / TypeScript)
  ├── HTTP fetch ──────────────────► Go Sidecar (127.0.0.1:5050)
  │                                   All Kubernetes + Helm operations
  └── IPC via contextBridge ────────► Main Process (Node.js)
                                        Terminal, settings, file dialogs,
                                        log streaming, port-forward, sidecar lifecycle

Renderer (src/renderer/)

  • Built with Vite + React + TypeScript + Tailwind CSS.
  • State managed by a single Zustand store (useAppStore) split into seven specialized slices:
Slice Responsibility
clusterSlice Context/namespace selection, RBAC denied tracking, provider detection trigger; on context switch clears inFlightSections and evicts the Go Prometheus cache
navigationSlice Active section, theme, sidebar width, search state, tour state (showTour), panel toggles (pluginsEnabled, gitopsEnabled, networkEnabled), unified log pod selection (unifiedLogsSelectedPods)
resourceSlice All 28 resource arrays, section loading with 30s TTL + module-level inFlightSections deduplication guard, dashboard fetch, resource navigation
operationSlice Scale/delete/YAML modals, exec session management, port-forward state
analysisSlice Security scanning (kubesec + trivy), scanInBackground state for background scans, owner chain, debug pods, Prometheus config
providersSlice Istio/Traefik/NGINX provider detection state
krewSlice Krew availability detection, curated plugin index (installed status), install/uninstall actions
  • Component Orchestration: The root App.tsx acts as a high-level shell. UI complexity is delegated to:
Component Responsibility
Layout Structural shell (Sidebar, Production banners, Sidecar alerts)
SectionRouter Routing between sections via two module-level dispatch maps (DIRECT_PANELS, LAZY_PANELS) keyed by ResourceKind; provider sections resolved via PROVIDER_SECTIONS set from config.ts
OverlayManager Floating terminals, YAML editors, and global modals
ErrorBoundary Declarative error recovery for critical UI zones
  • Talks to the sidecar via plain fetch() through IPC helpers (checkedSidecarFetch in src/main/sidecar/api.ts).

Go Sidecar (go-core/)

The sidecar is a standalone HTTP server compiled as podscape-core. It is the source of truth for all Kubernetes and Helm data.

Package Responsibility
cmd/podscape-core/main.go Route registration, startup, token auth middleware, CORS
internal/handlers/ HTTP handlers split across 12 files: resources.go (resource listers), operations.go (scale/delete/rollout), helm.go, security.go, network.go, tls.go, gitops.go, prometheus.go, ownerchain.go, customresource.go, providers.go; handlers.go holds the MakeHandler RBAC factory and shared helpers. operations.go exposes dynDelete/dynGet private helpers that resolve GVRs via k8sutil and automatically retry with KindGVRFallback on NotFound (e.g. HPA autoscaling/v2v1 on pre-1.23 clusters).
internal/k8sutil/ Canonical Kubernetes metadata: KindGVR (kind → GVR), KindGVRFallback (preferred version → older fallback, e.g. HPA autoscaling/v2v1), ClusterScopedKinds
internal/client/ Shared Kubernetes client initialisation (ClientBundle: REST config, clientset, apiextensions client)
internal/informers/ K8s shared informers — cache resource lists in-memory for fast reads; skips informers for denied resources
internal/store/ ClusterStore singleton: per-context ContextCache pool, active context pointer
internal/rbac/ CheckAccess — concurrent SelfSubjectAccessReview probe (list + watch, 8-goroutine pool); AllResources descriptor table
internal/ops/ Write operations shared between sidecar and MCP server: ListResource, GetResource, Scale, Delete, RolloutRestart, RolloutUndo, ApplyYAML. GVR resolution delegates to k8sutilDelete retries with KindGVRFallback on NotFound.
internal/portforward/ Manages active tunnels, streams events over WebSocket
internal/exec/ WebSocket-based container exec (PTY)
internal/logs/ WebSocket-based log streaming
internal/ownerchain/ Upward + downward owner reference traversal with 30s reverse-index TTL
internal/prometheus/ Prometheus auto-discovery via k8s service proxy; batch query with 60s TTL result cache + singleflight deduplication; atomic generation counter in ClearCache() prevents cross-context cache poisoning after context switch
internal/helm/ HelmRepoManager — repo list, chart search, version fetch, values, SSE install
internal/topology/ Cluster network topology graph (nodes → pods → services)
internal/providers/ Provider detection logic (Istio, Traefik, NGINX) used by the /providers endpoint

Context cache pool: each Kubernetes context gets its own ContextCache (clientset, informers, resource maps). Switching context restarts informers for the new context without affecting others already cached.

No-kubeconfig mode: if no valid kubeconfig is found at startup, the sidecar still starts the HTTP server and sets NoKubeconfig = true. /health returns 200 immediately so the renderer can show the KubeConfigOnboarding screen instead of an error dialog. After the user sets a kubeconfig path, the sidecar is restarted via window.sidecar.restart() IPC.

Token auth: the sidecar is launched with a randomly generated --token flag. Every request (except /health) must include the X-Podscape-Token header matching that token. The token is passed to the renderer via IPC and injected by checkedSidecarFetch.

RBAC probe: at startup and on every context switch, rbac.CheckAccess fires concurrent SelfSubjectAccessReview requests for all 28 tracked resource types checking both list and watch verbs. Results are stored in ContextCache.AllowedResources map[string]bool:

Value Meaning
nil Probe not yet run or failed — all resources treated as allowed (permissive)
empty map {} All resources denied
populated map Probed result; false = denied for that resource

Informers only register for resources where allowed[resource] != false. Each MakeHandler-built HTTP handler checks AllowedResources before serving; denied resources return 200 [] + X-Podscape-Denied: true. The renderer getResources IPC handler detects this header and throws RBACDeniedError, which loadSection catches to populate deniedSections: Set<ResourceKind> in the Zustand store. ResourceList renders an amber “Access denied” banner for denied sections instead of the generic empty state.

Sidecar Lifecycle & Crash Recovery

The sidecar subprocess is managed by src/main/sidecar/sidecar.ts. Several mechanisms work together to prevent false error dialogs and surface genuine crashes correctly.

shuttingDown flag: stopSidecar() sets shuttingDown = true before sending SIGTERM. The startup promise’s exitHandler checks this flag: if the process exits while shuttingDown is true it calls resolve() (not reject()), so the caller never sees a “sidecar failed to start” error dialog during normal quit.

Crash detection after startup: once startSidecar() resolves (i.e. /health returned 200 and startupComplete is set to true), a subsequent unexpected exit event on the child process sends a sidecar:crashed IPC message to the renderer. stopSidecar() resets startupComplete = false before stopping so an intentional stop does not trigger this notification.

isQuitting guard in index.ts: a module-level isQuitting flag is set in the before-quit handler. The catch block that wraps startSidecar() checks if (isQuitting) return before showing the error dialog — a second line of defence in case the shuttingDown flag race is lost.

Renderer crash recovery: Layout.tsx (via OverlayManager and NavigationSlice) listens for sidecar:crashed via window.sidecar.onCrashed(). When fired, it shows a red banner with a “Reconnect” button. Clicking the button calls window.sidecar.restart() (IPC channel sidecar:restart), which re-runs startSidecar() in the main process, and then reloads the renderer window.

API Channel Direction
window.sidecar.onCrashed(cb) sidecar:crashed Main → Renderer
window.sidecar.restart() sidecar:restart Renderer → Main

Main Process (src/main/)

File Responsibility
index.ts App bootstrap, splash window, sidecar start, BrowserWindow creation
sidecar/sidecar.ts Launch / monitor / kill the Go subprocess; expose sidecar:restart IPC
sidecar/api.ts checkedSidecarFetch — injects token, retries up to 20× with 500 ms delay
sidecar/auth.ts Generates the random per-session X-Podscape-Token
sidecar/runtime.ts Shared activeSidecarPort variable
ipc/kubectl.ts IPC handlers for log streaming, port-forward, file copy, owner chain, metrics
ipc/terminal.ts PTY terminal sessions via node-pty
ipc/helm.ts Helm IPC handlers — repo browser, SSE install relay
ipc/settings.ts Settings IPC handlers
ipc/dialog.ts Native file open/save dialogs
settings/settings_storage.ts Read / write ~/.podscape/settings.json
system/env.ts Augments subprocess PATH for cloud credential helpers
system/updater.ts Auto-updater checks and notifications
system/kubeProvider.ts Kubeconfig path resolution

Production Context Tracking

Podscape lets operators mark specific context names as “production” to guard against accidental changes.

Store fields (in clusterSlice, src/renderer/store/slices/clusterSlice.ts):

Field Type Description
prodContexts string[] List of context names treated as production
isProduction boolean Derived: true when selectedContext is included in prodContexts
setProdContexts(contexts) async action Updates both the store and ~/.podscape/settings.json atomically

isProduction is recomputed whenever selectContext runs or setProdContexts is called — there is no selector subscription overhead.

Visual indicators (managed in src/renderer/components/core/Layout.tsx):

  • Red ring: the root <div> gains ring-inset ring-4 ring-red-500/50 when isProduction is true.
  • Production banner: a fixed, centered banner reading “PRODUCTION CONTEXT ACTIVE” at z-index 10000.

Persistence: prodContexts is stored in ~/.podscape/settings.json under the prodContexts key (an array of strings). On app launch, settings_storage.ts merges saved settings over defaults before the renderer hydrates the store.

Preload (src/preload/index.ts)

Exposes six namespaced APIs to the renderer via contextBridge:

Namespace Purpose
window.kubectl All k8s operations, port-forward, log streaming, file copy, owner chain, Prometheus queries + prometheusFlushCache
window.helm Helm release operations and repo browser
window.exec PTY exec-into-container sessions
window.settings Read / write app settings
window.kubeconfig Kubeconfig file path selection
window.dialog Native file open/save dialogs
window.sidecar Sidecar restart (used by kubeconfig onboarding)

Provider Detection (Istio / Traefik / NGINX)

On every successful context switch the renderer fires fetchProviders() against the sidecar’s /providers endpoint. The sidecar uses the Kubernetes discovery API and IngressClass controller fields to detect installed service meshes and ingress controllers:

Provider Detection method
Istio networking.istio.io API group present
Traefik v3 traefik.io API group present
Traefik v2 traefik.containo.us API group present
NGINX Inc k8s.nginx.org API group present
NGINX Community IngressClass controller field contains ingress-nginx

The providers value in the Zustand store drives conditional sidebar groups. fetchProviders captures the context at call time and discards results if the context changed while the request was in-flight (stale-context guard). On context switch, all provider flags reset to false and any active provider section (member of PROVIDER_SECTIONS in src/renderer/config.ts) auto-navigates to dashboard to prevent stale CRD fetches.

MCP Server (podscape-mcp)

podscape-mcp is a standalone binary built from go-core/cmd/podscape-mcp/. It exposes the Kubernetes cluster as MCP (Model Context Protocol) tools for AI assistants such as Claude and Cursor. It is independent of the Electron app and can be run directly from the command line.

It reuses the internal/client, internal/ops, and internal/helm packages from the sidecar to avoid duplicating Kubernetes client logic.


Startup Sequence

1. app.whenReady()
2. createSplashWindow()          ← frameless Kubernetes-animated splash
3. startSidecar()                ← spawns podscape-core, polls /health every 500ms
   ├─ if kubeconfig missing:
   │   sidecar enters no-kubeconfig mode → /health returns 200 immediately
   └─ if kubeconfig found:
       a. HTTP server starts (binds to 127.0.0.1:<port>)
       b. rbac.CheckAccess — concurrent SAR probe, 10s deadline
          → writes AllowedResources into initial ContextCache
       c. informers.InitInformers — only registers informers for allowed resources
       d. ContextCache.HasData = true → /health returns 200
4. createWindow(onReady)
   └─ ready-to-show → splash.destroy(), main window shown

Architecture Diagram

flowchart TB
    subgraph R["Renderer — React / TypeScript"]
        Shell["App Shell"]
        Store["Zustand Store"]
        Preload["Preload / contextBridge"]
    end

    subgraph M["Main Process — Node.js"]
        IPC["IPC Handlers"]
        SM["Sidecar Manager"]
        Sys["System"]
    end

    subgraph S["Go Sidecar — podscape-core  :5050"]
        HTTP["HTTP Handlers"]
        Pkg["Internal Packages"]
    end

    MCP["MCP Server\npodscape-mcp"]
    K8S[("Kubernetes API")]
    Shell2["Native Shell"]
    Ext["External Services"]

    Preload -->|"IPC"| IPC
    IPC --> SM
    SM -->|"spawn"| S
    SM -->|"HTTP + token"| HTTP
    IPC -->|"node-pty"| Shell2
    HTTP --> Pkg
    Pkg -->|"client-go"| K8S
    Pkg -->|"WebSocket"| Preload
    HTTP --> Ext
    Sys --> Ext
    MCP -->|"client-go"| K8S

Application Security

Electron hardening

Setting Value Effect
nodeIntegration false Renderer cannot access Node.js APIs
contextIsolation true Preload and renderer run in separate JS contexts
sandbox true Renderer process is OS-sandboxed
setWindowOpenHandler deny + openExternal New window requests are blocked; links open in the system browser instead

Sidecar token auth

At startup, sidecar/auth.ts generates a random 64-character hex token (crypto.randomBytes(32)). It is passed to the sidecar as the --token flag and injected into every HTTP request by checkedSidecarFetch as the X-Podscape-Token header. The sidecar rejects any request (except /health) that omits or mismatches the token. The token is never written to disk and is unique per app session.

Secret masking

The bulk /secrets endpoint returns only key names — values are replaced server-side before the response leaves the sidecar. A value is only transmitted when the user explicitly clicks “Reveal” for a specific key, which calls the /secret/value?name=&namespace=&key= endpoint.

Path traversal protection

sanitizeCPToLocalPath in go-core/internal/handlers/operations.go validates every local path used by the file-copy (/cp/to, /cp/from) endpoints. It resolves the input with filepath.Abs + filepath.EvalSymlinks (following all symlinks) before confirming the real path starts within the user’s home directory or the OS temp directory. Absolute paths from the native file dialog are accepted; only paths that escape the allowed roots are rejected.

Dependency security pins

package.json overrides pins transitive dependencies that have known CVEs to safe versions:

Package Reason
dompurify ≥ 3.4.0 mXSS, prototype pollution, ADD_ATTR bypass fixes
@xmldom/xmldom ^0.8.12 XML parser security fixes
lodash ^4.18.1 Prototype pollution
brace-expansion ^1.1.13 / ^2.0.3 ReDoS

RBAC

The sidecar runs SelfSubjectAccessReview against all 28 tracked resource types at startup and on every context switch. Resources the current user cannot list or watch return 200 [] with X-Podscape-Denied: true — the renderer shows an “Access denied” banner rather than an error. No data from denied resources ever reaches the renderer.


Key Constants

All renderer-to-sidecar fetch calls use constants from src/common/constants.ts:

SIDECAR_HOST    = '127.0.0.1'
SIDECAR_PORT    = 5050
SIDECAR_BASE_URL = 'http://127.0.0.1:5050'
SIDECAR_WS_URL   = 'ws://127.0.0.1:5050'

Copyright © 2026 Coding Protocols Private Limited. Distributed under the Apache 2.0 license.