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-uiimage 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 envelopeschemaVersion. - 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-ui— not 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-uiimage decouples UI release cadence from the controller binary. - Hybrid CRD status keeps condition parity with
kubectl describewithout bloating the Read API for MVP. - The biggest pre-UI investment is the Read API +
InventoryReaderinterface, not the SPA.
Negative¶
- Drift-over-time requires a historical store — headline portal differentiator lands on
postgres/parquetadapters 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 image — ADR-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 v4 — ADR-0410 |
| OQ-7 | Bundle budget enforcement? | Fail CI on breach — ADR-0410 |
| OQ-8 | Visual regression in CI? | Nightly required — ADR-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¶
- ADR-0404: Inventory HTTP API authentication
- ADR-0405: Export data contract and schema versioning
- ADR-0409: Kollect UI deployment
- ADR-0410: UI engineering and quality gates
- ADR-0411: Read API extensions for UI
- ADR-0504: Operator runtime modes — optional
kollect-serversplit at scale - ROADMAP.md § Read API + UI console