Skip to content

ADR-0408: Read API and UI architecture

A stable, versioned Read API as the UI's contract for inventory rows, with a pluggable backing store (memory → Postgres → Parquet) and a separate read-only SPA — so one console serves both a zero-infra cluster view and a multi-cluster portal without violating the read-model thesis.

Theme: 04 · Export & sinks (read side) · Status: Current (accepted 2026-06-05; was Exploring)

Context

An inventory product lives or dies on whether people can see their inventory. Argo CD's adoption is driven far more by its UI — a live, filterable overview of status, topology, and drift — than by its reconciler. For Kollect, a read-only UI (searchable resource catalog, export/freshness health, multi-cluster rollup, attribute drift over time) is a higher-leverage adoption investment than more sink backends or advanced collection features.

But Kollect has a tension Argo CD does not. Our thesis (ARCHITECTURE.md, REQUIREMENTS.md) is that consumers must not read the live kube-apiserver for inventory rows at scale — they read the durable export (the read model). A UI that hammers the apiserver for catalog data, or couples portal availability to the controller process, would violate that. Argo CD's UI is intentionally coupled to its controller; Kollect's inventory SPA must remain a consumer of the read model.

Maintainer decisions (2026-06-05, rev 2 engineering spec + UX approach) resolve the former open questions (OQ-1–12). Deployment, engineering gates, and Read API extensions are split into companion ADRs so this record stays the architecture spine:

  • ADR-0409 — separate kollect-ui image and Helm subchart
  • ADR-0410 — monorepo ui/, stack, CI pyramid
  • ADR-0411 — pagination, filters, envelope, CRD status hybrid

Decision

1. The Read API is the contract for inventory rows — not the store

The UI depends on a stable, versioned Read API for inventory catalog data. It extends the existing inventory HTTP surface (ADR-0103, ADR-0404):

  • Versioned (/v1alpha1/…), OpenAPI-described (openapi/v1alpha1/inventory.yaml), returning the export data contract (ADR-0405) with its envelope schemaVersion.
  • List/filter/search over inventories and items (by cluster, namespace, GVK, target, name, attribute); per-inventory/sink export status and freshness; multi-cluster rollup views (ADR-0305).
  • Auth on the Read API: Kubernetes TokenReview + SAR when a bearer token is present (ADR-0404). The MVP SPA has no login shell — auth offload via oauth2-proxy at ingress is post-MVP (ADR-0409).

Hybrid Kubernetes API (OQ-3): the UI may call the Kubernetes API for CRD conditions and metadata (KollectTarget, KollectInventory, KollectScope, etc.) alongside the Read API. It must never use kube list/watch for inventory rows at scale (FR-READ-1). See ADR-0411 for the recommended MVP split.

2. Distinct InventoryReader — not sink Backend

Behind the Read API sits a backing-store adapter — a distinct InventoryReader interface, not the sink Backend inverted (ADR-0406, OQ-11). Sink backends write projections; the reader reads them (or the in-memory canonical store) through one contract:

Adapter Use Trade-off
memory (default for first UI) Operator's in-memory Store — live, single-cluster, zero extra infra No history; bounded by operator memory/availability
postgres Scale + history/drift Needs a DB
parquet (DuckDB/object store) Scale, queryable, no DB server Snapshot granularity; compaction

The same indirection lets one SPA serve a live console or a scale portal by swapping the adapter — no UI rewrite. Portal mode reads Postgres/Parquet through the Read API adapter, never ad-hoc SQL from the browser.

3. Real-time updates (OQ-4)

Adapter Mechanism
memory SSE via GET /v1alpha1/inventory/watch (operator Store watch)
postgres / parquet Poll (default 30 s); SSE optional later

The SPA uses TanStack Query cache invalidation on SSE events or poll interval.

4. SPA is static, read-only, and separately deployed (OQ-1, OQ-12)

A React single-page app in monorepo ui/ (OQ-5) that:

  • Talks to the Read API for inventory rows and export metadata.
  • May talk to the Kubernetes API for CRD status/conditions only (hybrid).
  • Is read-only in v0.2 — observability console; no Target create/apply forms; onboarding is copy-YAML + docs links.
  • Ships as a separate static container image ghcr.io/konih/kollect-uinot embedded in the operator binary (ADR-0409).

Stack, testing pyramid, and bundle budget: ADR-0410.

5. Ingress and exposure (OQ-10)

Inventory HTTP and the UI are off ingress by default. Operators enable Read API + UI ingress explicitly when cluster-network access or auth offload is configured. Dev workflows use port-forward.

6. Phasing

Aligned with ROADMAP.md and engineering spec §12:

Milestone Backend prerequisite Frontend deliverable
v0.5.x Harden Read API: filters, schemaVersion, export status in OpenAPI (ADR-0411) Monorepo ui/ scaffold; MSW mocks; contract + unit tests
v0.6.x Memory adapter complete; Read API SAR-gated UI foundation: store shell, SSE wiring
v0.7.x Memory adapter stable Read-only MVP SPA in separate kollect-ui image: Overview, Inventory, Targets/Sinks lists + detail drawers (no Target create forms); onboarding = copy-YAML + docs; no auth UI
v0.8.x Postgres Read adapter; hub merged metadata API Portal mode begins
v0.9.x Parquet adapter; drift queries Portal mode, drift chart, multi-cluster picker; oauth2-proxy at ingress + cross-cluster auth spike
v0.10.0 Presentation soak Demo-ready release
Phase 3 KollectScope deniedNamespaces UI API Scope comparison; policy violation explainer; optional BFF
Phase 4 Custom resource metrics in Read API Metrics sparklines in Target detail

Consequences

Positive

  • The UI is decoupled from storage and from unbounded apiserver inventory reads — the read-model thesis holds at every tier.
  • Separate kollect-ui image decouples UI release cadence from the controller binary.
  • Hybrid CRD status keeps condition parity with kubectl describe without bloating the Read API for MVP.
  • The biggest pre-UI investment is the Read API + InventoryReader interface, not the SPA.

Negative

  • Drift-over-time requires a historical store — headline portal differentiator lands on postgres/parquet adapters only.
  • Hybrid K8s API calls require a browser-accessible apiserver endpoint (or post-MVP BFF) for CRD status; cluster-network / port-forward assumptions apply in MVP.
  • Read API extensions (ADR-0411) block v0.7 UI feature work until shipped (planned v0.5 band).

Resolved questions (formerly open)

# Question Resolution
OQ-1 Embed UI in operator vs separate image? Separate kollect-ui imageADR-0409
OQ-2 Browser auth in production? oauth2-proxy at ingress post-MVP; MVP no auth in frontend
OQ-3 UI calls Kubernetes API for CRD status? Hybrid allowed — CRD conditions/metadata only
OQ-4 Real-time updates? SSE (memory) + poll (postgres)
OQ-5 Monorepo vs separate repo? Monorepo ui/ADR-0410
OQ-6 CSS strategy? Tailwind v4ADR-0410
OQ-7 Bundle budget enforcement? Fail CI on breach — ADR-0410
OQ-8 Visual regression in CI? Nightly requiredADR-0410
OQ-9 Cross-cluster portal auth? Deferred — no auth in MVP UI
OQ-10 Inventory HTTP on ingress by default? Off — explicit opt-in
OQ-11 Reader vs sink Backend? Distinct InventoryReader
OQ-12 Target create form in MVP? Read-only UI — no create forms

Client UI state (v0.7 band)

Inventory filter prefs, column visibility, and drawer selection use Zustand slices in ui/src/store/ with Vitest unit tests — see ADR-0410 §2. Server inventory rows and status responses stay in TanStack Query; URL search params are the source of truth for shareable inventory filters.

See also