Live Kubernetes resource updates for browser apps.
krm-stream turns a Kubernetes watch into a small, browser-safe stream. It ships a Go gateway, a
headless TypeScript store with zero runtime dependencies, and shared conformance fixtures, so your
product can show live cluster state while people are editing it.
Yes, if:
- You want to consume a Kubernetes watch from a browser, without handing the browser a cluster credential or turning on CORS across your API server.
- You want to live-edit Kubernetes resources, where a concurrent server change is merged into what the user is typing rather than clobbering it, and a genuine conflict is surfaced instead of silently resolved. That is the three-way merge.
- You want to bound the number of real watches on your API server. Ten tabs on one namespace should be one upstream watch, not ten.
- You are building a product, not a Kubernetes dashboard. The store is headless and picks no UI framework.
Probably not, if:
- You just want a generic three-way merge library. This one knows what a
resourceVersionis, thatspec.containersis keyed bynameand not by index, and that a redacted field must never be written back. That knowledge is the whole point; if you do not want it, it is weight. - You want a ready-made Kubernetes dashboard. Use Headlamp. See alternatives.
- You want to write to the cluster from the browser. krm-stream is the read-and-edit half: it hands your application a validated merge patch, and your application performs the write. Though if you are doing that, you probably want this library anyway, because it is the thing that tells you the patch is safe to apply. See saving edits safely.
KRM is the Kubernetes Resource Model: the shape every Kubernetes object has (apiVersion,
kind, metadata, a desired spec, an observed status). Custom resources use the same shape,
which is why this works for your product's own objects, a Database, a FeatureFlag, a Tenant,
and not only for cluster infrastructure.
Never touched a cluster? The glossary for frontend developers.
Kubernetes already has a good change feed: a watch, documented under
efficient detection of changes.
A browser cannot use it directly. Watching requires a cluster credential, the API server serves no
CORS, and a watch hands back whole objects including Secret data. The gateway holds the credential,
withholds what the browser should not see, and re-frames the stream as SSE that EventSource reads
natively. It also shares one upstream watch per scope, so ten tabs are not ten watches on the API
server.
Why a gateway works through this in full.
flowchart LR
person["Person\nediting a form"]
browser["Your browser app\n@configbutler/krm-stream"]
store["LiveResourceStore\nbase + local draft + live update\nthree-way merge"]
host["Your Go application\nidentity, authorization, scope\nprojection and redaction"]
api["Kubernetes API\nand resource watch"]
resources["KRM resources\napps, policies, databases,\nplatform and business objects"]
save["Your save endpoint\nvalidates and applies a patch"]
person --> browser --> store
store <-->|"snapshots and live deltas (SSE)"| host
host <-->|"read and watch"| api --> resources
store -->|"draft, conflicts, merge patch"| browser
browser -->|"explicit save"| save --> api
classDef person fill:#fff3cd,stroke:#d39e00,color:#3f3000;
classDef browser fill:#dff3ff,stroke:#1677a4,color:#062f45;
classDef host fill:#e4f7e8,stroke:#27834c,color:#113d23;
classDef api fill:#f8e0ef,stroke:#a83970,color:#4b1230;
classDef store fill:#ede7ff,stroke:#6750a4,color:#2d1e5c;
class person person;
class browser,store browser;
class host,save host;
class api,resources api;
The library owns the read stream and browser reconciliation. Your application owns identity, authorization policy, Kubernetes credentials, and writes. The browser never receives a Kubernetes credential or a raw API-server URL.
There are two halves, and they are usually two different people.
No bundler, no framework, no Kubernetes client. EventSource is native, and the store is plain ESM:
import { LiveResourceStore, connectWithEventSource, resourceStreamURL } from "@configbutler/krm-stream";
const store = new LiveResourceStore();
connectWithEventSource(
resourceStreamURL("/resource-stream/v1", {
target: "production",
version: "v1",
resource: "configmaps",
namespace: "app",
}),
store,
{
onChange: (change) => render(change.uid), // what moved, and which resource it moved on
},
);
// The user edits. The server keeps changing underneath them. Neither wins by accident.
store.setValue(uid, ["spec", "replicas"], 3);
store.conflicts(uid); // paths where the server disagreed with an edit the user actually made
store.patch(uid); // an RFC 7386 merge patch of just their changes, or nullIf you have no bundler at all and vendor the library by copying it, import
@configbutler/krm-stream/bundle: the same API in one file.
Mount a scoped stream endpoint in an existing Go application:
mux.Handle("/resource-stream/v1", gateway.Handler(gateway.Options{
Principal: func(r *http.Request) (gateway.Principal, error) { return userFromSession(r) },
Authorizer: authorizeScope,
Clients: func(_ context.Context, _ string, p gateway.Principal) (gateway.Backend, error) {
return kube.NewBackend(dynamicClientFor(p.(*User))), nil
},
Scopes: gateway.ScopePolicy{
Targets: []string{"production"},
Resources: []gateway.GroupResource{
{Resource: "configmaps", Scope: gateway.ResourceScopeNamespaced},
},
},
Projection: gateway.ProjectionFull,
}))The Go side owns identity, authorization, the Kubernetes credential, and the scope a caller is allowed to ask for. It never lets the browser choose which cluster to talk to.
LiveResourceStore keeps server truth and the local draft separate, reconciles live updates with a
three-way merge, records conflicts, and builds RFC 7386 merge patches. Your application applies any
patch through its own save endpoint, which is the one place a write can happen.
| 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 SSAR authorizer. |
@configbutler/krm-stream |
Official dependency-free ESM client store and transports. |
krm-stream@0.1.0 |
Deprecated, frozen compatibility name claim. Use the scoped package instead. |
spec/v1.md |
Normative protocol contract. |
conformance/ |
Shared fixtures exercised by the Go gateway and TypeScript client. |
- No browser token handling or raw API-server URLs.
- No authorization system: the host provides
Principal,Authorizer, andClientFor. - No write endpoint: hosts validate and apply their own patches.
- No framework dependency in the browser client.
- No
client-godependency in the core gateway.
The important safety rule is simple: a projected or redacted field must never be written back by a
browser. Use gateway.ValidateMergePatch in the host save handler.
- Glossary for frontend developers: the Kubernetes vocabulary you actually need, and where each word shows up in the library.
- Why a gateway: why the browser cannot watch the API server, and why watches are shared.
- Adopting krm-stream: same-origin cookie, bearer-token, and shared-watch setups.
- Authentication and authorization: identity and RBAC boundaries.
- Saving edits safely: patch validation and host write responsibilities.
- Operating krm-stream: metrics, alerts, and runtime controls.
- Client state model: drafts, conflicts, redactions, and keyed lists.
- Alternatives and prior art: how this differs from Kubernetes clients, browser dashboards, and config-as-data systems.
- Releasing: release workflow and publication prerequisites.
The project is pre-1.0. Protocol and API changes may still be made before 1.0.
- Go 1.26 for the gateway.
- Node 22 for client development and tests.
- Kubernetes 1.35+ for strict resource-version ordering.
OrderingLenientsupports known non-conformant or aggregated APIs at the cost of per-object monotonic ordering.
task fixtures-check
task test
task lint
task build-clientSee CONTRIBUTING.md for the fixture and test workflow. Licensed under Apache-2.0.