Efficient live Kubernetes views for browser applications, with optional editing.
krm-stream helps applications watch Kubernetes resources while controllers and other users keep
changing them. Its headless TypeScript client adds connection lifecycle, recovery and a live resource
store, reading native Kubernetes collections through a host proxy or projected views from the
gateway, and reconciles incoming changes with local drafts when a page needs editing. Its gateway
delivers a defined resource view, withholds selected values, suppresses irrelevant updates and
optionally shares upstream watches.
Your application supplies authentication, authorization policy, Kubernetes credentials, UI and writes. The browser client has zero runtime dependencies and chooses no UI framework.
Kubernetes already provides a change feed. krm-stream adds a browser lifecycle and resource-view contract, reducing work at different points:
| Capability | Benefit | Boundary |
|---|---|---|
| Scope | Watch a resource kind, namespace, name or allowed label selector | The host authorizes the scope; a selector does not grant access |
| Projection and redaction | Deliver the fields a view needs; full/spec views withhold Kubernetes Secret values | Secret key paths and change revisions remain visible; arbitrary sensitive CRD fields are not automatically redacted |
| Update suppression | Avoid downstream events when projected content and redaction records are unchanged | The gateway still receives upstream changes; fewer events do not guarantee a current write version |
| Optional watch sharing | Use one upstream watch for matching scopes on the same shared backend | Every subscriber is authorized separately and receives its own snapshot and updates |
Choose a view explicitly:
| View | Delivered content | Updates suppressed |
|---|---|---|
krm-full/v1 (default) |
Resource including status, with Secret values withheld | Bookkeeping-only changes |
krm-spec/v1 |
Full view with status omitted | Bookkeeping-only and status-only changes |
krm-raw/v1 |
Secret values included when host policy permits | Bookkeeping-only changes |
All three remove metadata.managedFields and the last-applied-configuration annotation.
krm-raw/v1 is still a projection. A hidden Secret rotation produces a redaction update in full/spec.
Each current gateway connection starts with a complete snapshot, then follows visible changes. On reconnect, a fresh snapshot repairs missed changes and deletes; resources are pruned only when it completes. This is a live state feed: intermediate updates may be coalesced. See watching resources.
Start with the resource state and guarantees the page needs; framing is an implementation detail.
| Source | What the page receives | Connector |
|---|---|---|
| Native through a host proxy | Original authorized resources, with the shared client lifecycle and store | connectNativeWatch: LIST, then WATCH; a reconnect resumes the WATCH, and only an expired history lists again; edits are written back through the same proxy |
| Gateway | Projected, redacted or suppressed views and optional upstream watch sharing | connectResourceStream, delivered over SSE |
Native is the straightforward starting point for hosts that already proxy Kubernetes. It delivers what the proxy returns, Secret values and machinery fields included; it provides no projection, redaction, suppression or watch sharing. Both connectors share connection state, errors, cancellation, bounded recovery and state events. Use a separate store per source, scope and login identity, and never fall back from a refused gateway view to native access.
import {
LiveResourceStore, readOnlyPolicy, applyStreamEvent,
connectNativeWatch, nativeCollectionURL,
} from "@configbutler/krm-stream";
const store = new LiveResourceStore(readOnlyPolicy);
const stopRendering = store.subscribe(() => renderResources(store));
const connection = connectNativeWatch(
// → /k8s/apis/apps/v1/namespaces/app/deployments?labelSelector=tier%3Dweb
nativeCollectionURL("/k8s", {
group: "apps", version: "v1", resource: "deployments", namespace: "app", labelSelector: "tier=web",
}),
event => applyStreamEvent(store, event),
);
const stopConnection = connection.subscribe(state => renderConnection(state.status));
connection.closed.catch(reportApplicationError);
// On view disposal:
stopRendering();
stopConnection();
connection.close();The host mounts the API server's paths under a base such as /k8s and owns credentials, routing and
which collections a user may read. The connector lists the complete collection, delivers it as a
snapshot and becomes live once the WATCH from the collection's resourceVersion is accepted. A
dropped or ended watch resumes from the last event applied, without a new snapshot, so drafts stay
and missed changes arrive as events. HTTP or in-stream 410 (expired history) lists again within the
bounded retry budget, and the store prunes only when that snapshot completes; 401, 403 and other 4xx
are terminal. The connector does not paginate: a LIST that returns a continuation token is refused. The
native viewer example runs this against kubectl proxy.
import {
LiveResourceStore, readOnlyPolicy, applyStreamEvent,
connectResourceStream, resourceStreamURL,
} from "@configbutler/krm-stream";
const store = new LiveResourceStore(readOnlyPolicy);
const stopRendering = store.subscribe(() => renderResources(store));
const connection = connectResourceStream(
resourceStreamURL("/resource-stream/v1", {
target: "production", version: "v1", resource: "configmaps", namespace: "app",
}),
event => applyStreamEvent(store, event),
);
renderConnection(connection.state.status);
const stopConnection = connection.subscribe(state => renderConnection(state.status));
connection.closed.catch(reportApplicationError);
// On view disposal:
stopRendering();
stopConnection();
connection.close();Viewers use LiveResourceStore(readOnlyPolicy); a dedicated read-only store is deferred.
The connector delivers state events independently of editing. It uses same-origin cookies by default,
exposes connection state and bounded retries, and stops on terminal refusals. Apply each event
synchronously. The client README explains lifecycle and errors.
For a browser without a bundler, the same API is available in one file through
@configbutler/krm-stream/bundle.
Use new LiveResourceStore() for an editor. It keeps the last delivered server object separate from
the person's draft. Incoming changes update untouched fields, preserve local edits and record
conflicts when both sides changed the same editable field differently.
For example, someone changes a Deployment's image while an autoscaler changes its replicas. The replicas follow the server and the image edit stays. If another person changes that image to a different value, the editor keeps the local value and exposes the disagreement for review.
const store = new LiveResourceStore(); // use this store in the connection setup for an editor
store.setValue(uid, ["data", "message"], "hello"); // ConfigMap field edit
store.conflicts(uid); // disagreements to resolve before Save
const intent = store.captureSave(uid); // detached { uid, resourceVersion, patch }
// Your save controller submits this intent after review while the connection is live.The default policy allows spec, labels, annotations, data and stringData; status, immutable
metadata and redacted paths remain read-only. Under every policy, metadata.managedFields and the
last-applied annotation stay read-only too. A host can narrow the policy for its form.
The intended Save flow is explicit: capture the patch, UID and resource version together; have the host authorize and validate it; apply a conditional merge PATCH; then observe the projected result through the stream or a guarded projected read. Preserve typing made after Save. A version rejection can occur without any field conflict, because suppressed updates still advance Kubernetes versions. The current recovery is a guarded read, review and another deliberate Save. A native store follows the same flow through its host proxy: the browser sends the merge PATCH with the captured UID and version inside it, and the proxy validates it before forwarding.
A dirty draft, an accepted write and application progress are separate states. A successful PATCH can precede its watch observation, and neither proves a workload has finished rolling out.
Use the editor state model for reconciliation, conflict resolution and arrays, and saving edits safely for the complete host-owned write contract. The conditional-save example executes that contract, and the native editor example its native counterpart.
flowchart LR
api["Kubernetes API"]
proxy["Your native proxy<br/>Credentials, routing and write checks"]
gateway["Go gateway<br/>Scopes, views and optional sharing"]
connector["Fetch connector<br/>State events and recovery"]
store["Resource store<br/>Live state and optional drafts"]
ui["Your list, viewer or form"]
save["Your save endpoint<br/>Authorize, validate and conditionally PATCH"]
api -->|"LIST and WATCH"| proxy
proxy -->|"Native watch JSON"| connector
api -->|"Snapshot and watch"| gateway
gateway -->|"SSE"| connector
connector --> store
store --> ui
ui -->|"Local edits"| store
ui -->|"Captured save intent"| save
save --> api
ui -->|"Native conditional PATCH"| proxy
proxy --> api
A page uses one source per store: the native proxy or the gateway. The gateway runs inside your Go application. Per-user backends let Kubernetes authorize the caller's reads; a shared backend uses a service identity and requires checks for each subscriber. Sharing reduces duplicate upstream work; access controls and host limits govern who can consume it. Gateway upstream continuation and improved save progress during suppressed churn are proposed work. New browser connections receive a fresh snapshot under the current protocol.
If you already have a Go host, follow adopting krm-stream. For a ready-made host, krm-foyer integrates sign-in, sessions, native API proxying and krm-stream gateway hosting. The host's dependency version determines which APIs are available.
KRM means the Kubernetes Resource Model: apiVersion, kind, metadata and kind-specific fields
such as spec, status or ConfigMap data. Custom resources follow the same conventions. See the
frontend glossary and alternatives for context.
| Package | Purpose |
|---|---|
github.com/ConfigButler/krm-stream/gateway |
Dependency-free Go stream gateway and SSE handler |
github.com/ConfigButler/krm-stream/gateway/kube |
Optional client-go backend and SubjectAccessReview authorizer |
@configbutler/krm-stream |
Dependency-free ESM native and gateway connectors, and the resource/editor store |
| spec/v1.md and conformance | Shared normative contract and executable fixtures |
- Watching resources: scopes, views, suppression, sharing and recovery.
- Adoption: host and browser wiring.
- Editor state model: drafts, conflicts, redactions and arrays.
- Saving: conditional writes, recovery and user-facing outcomes.
- Authorization and operations: identity, revocation and runtime limits.
- Examples: native viewer and editor, gateway browser demo, conditional editor, recovery recipes and Vue integration.
- Delivery plan: completed work, open work, ordering and dependencies.
- Upgrading from 0.7 and releasing.
The project is pre-1.0; protocol and API changes may still be made before 1.0.
Go 1.27.1 and Node 24 are required for development. Kubernetes 1.35+ supports strict resource-version
ordering; OrderingLenient accommodates known non-conformant or aggregated APIs with a reduced
per-object monotonicity guarantee.
task fixtures-check
task test
task lint
task build-clientSee CONTRIBUTING.md. Licensed under Apache-2.0.