Resource Meta-API — self-describing resources, one contract for every client
Status: DESIGN (proposal). Owner: platform. 2026-07-23.
Note (2026-07-29): the CE/EE edition split referenced below has been removed — shc is a single binary/package, and paid capabilities are gated by license entitlements (tiers), not build editions. Read “CE” as the free tier, “EE” as license-gated surfaces; the CE⊆EE dual-binary drift gate and the “after the CE/EE flip (B7/B9)” sequencing are obsolete. The registration-absence property still holds per-license posture via route registration.
Motivation
Section titled “Motivation”Every client that renders SHC resources today hardcodes what it knows:
- The generated
pkg/apiclientknows the surface at generation time. - The HUD compiles its row types, panels, and columns in.
- The planned MCP server (
pat-mcp-design.md) gates its tool list onGET /api/capabilitiesbut still ships a hand-picked tool set. - Any third-party dashboard, TUI, or script starts from zero.
The useful property (proven at scale by k8s’s self-describing API, but not unique to it): clients discover resource kinds at runtime, the server supplies table columns, and one generic client renders everything — including kinds added after the client shipped.
This proposal adds a small meta-layer over the existing API: discovery + uniform envelopes + server-supplied column hints + watch association. No new resource semantics, no new state, no second API.
Ruling context (user, 2026-07-23): SHC will NOT expose a k8s-compatible
API for external k8s tooling — that idea is dead (Non-goals #1). The k9s
value lands by porting its UX ideas into the HUD (hud-k9s-ideas.md, EE);
this meta-API is the server-side infrastructure the MCP server and generic
clients need regardless, and its actions descriptor is the server half of
HUD idea #4 (per-row-type action registry).
Non-goals (settled up front)
Section titled “Non-goals (settled up front)”- No k8s wire compatibility. kubectl/k9s/client-go speak the exact
apimachinery protocol — kubeconfig auth,
/apisGVR discovery,resourceVersionwatch semantics, content-negotiatedTableresponses, strategic-merge-patch. Serving that means impersonating a kube-apiserver and chasing client-go skew forever (kcp and Rancher steve are the cautionary precedents — both are huge). SHC is not Kubernetes and will not mock being Kubernetes. If a k8s facade ever earns its keep (GitOps interop), it is a thin adapter built ON this meta-API, priced then. - No full Frappe compatibility. The API is already Frappe-shaped
(
/api/method/shc.*,/api/resource/<Kind>) and stays that way; chasing Frappe’s filter grammar / doctype-meta endpoints buys tooling nobody asked for and a partial-compat bug farm. - No plugin runtime. Features-in-core doctrine. The meta-API makes clients generic, not the server extensible.
Design
Section titled “Design”1. Discovery — GET /api/resources
Section titled “1. Discovery — GET /api/resources”One endpoint returning every registered resource kind with:
{ "version": "v1", "kinds": [ { "kind": "Stack", "plural": "stacks", "scope": "deployment", // system|tenant|deployment|node — the 5-level scoping model "verbs": ["list", "get", "delete"], "routes": { "list": "/api/resource/Stack", "get": "/api/resource/Stack/{name}" }, "columns": [ {"name": "Name", "field": "name", "priority": 0}, {"name": "Tenant", "field": "tenant", "priority": 0}, {"name": "Health", "field": "health", "priority": 0, "render": "status"}, {"name": "Services","field": "services", "priority": 1} ], "watch": {"topic": "stack.*"}, // events-socket topic pattern, if any "actions": ["backup", "restart"] // verb routes beyond CRUD (method-style) } ]}- Generated from the route registry, the same source
gen-apiclient.gowalks — the descriptor is derived, never hand-maintained. Kinds without registered routes cannot appear; kinds with routes cannot hide. - Column hints are the k8s
Tableidea without the protocol: the server says how to render a list sensibly;priority > 0marks columns a narrow client may drop.rendernames a client-side formatter vocabulary (status,age,bytes,count) — small, versioned, documented. - Edition by absence, automatically. Discovery reflects what is
registered in this binary. CE never lists EE-only kinds because their
routes are never mounted (registration absence, A5–A9 pattern) — no
edition logic in the meta-layer at all. The existing
/api/capabilitiessnapshot stays the coarse feature probe (cli/client_wiring.go:75round-trip);/api/resourcesis the fine-grained render contract.
2. Uniform list/get envelopes
Section titled “2. Uniform list/get envelopes”/api/resource/<Kind> responses standardize on one envelope (items +
paging cursor + kind echo). Most resource routes already return
list-shaped JSON; this step is normalization, not redesign. Divergent
responses get adapters at the route layer; the generated apiclient regen
picks the envelope up mechanically.
3. Watch — associate kinds with the existing events stream
Section titled “3. Watch — associate kinds with the existing events stream”No new transport. The events module + notification socket already publish
typed events; the descriptor’s watch.topic names the subscription pattern
a client uses to keep a listed kind live. Clients that don’t subscribe just
poll list. (k8s-style resourceVersion resume semantics are explicitly
NOT promised — the stream is a cache-invalidation hint, re-list on gap.)
4. Actions
Section titled “4. Actions”Non-CRUD verbs (backup now, restart, rotate) surface as actions naming
their /api/method/... routes. This is what makes a generic client able to
offer k9s-style per-row actions without compiling them in — and it is the
server-side complement of the HUD per-row-type action-registry idea
(hud-k9s-ideas.md #4).
Consumers
Section titled “Consumers”| Client | What it gains |
|---|---|
| MCP server (planned, CE) | Tool list = discovery output; new kinds appear as tools without an MCP release |
| HUD (EE) | Row types/columns/actions from the shared registry instead of parallel compiled-in tables |
| Generic web/TUI/scripts | list any kind + sensible tables + live updates from day one |
| Future k8s-facade adapter | Translation source, if ever justified |
CE/EE posture
Section titled “CE/EE posture”The meta-layer itself is CE (it describes whatever is present; in CE that is the CE surface — nothing to gate). EE kinds and actions appear in EE builds purely by registration. The one rule: no kind/action may be listed whose route is not mounted in the same binary — enforced by construction (derived from the registry) plus a drift test.
Versioning
Section titled “Versioning”/api/resources is a public, versioned contract from day one —
"version": "v1" in the payload, additive evolution only, breaking changes
bump the version. The exported ≠ promised Go-API doctrine does NOT apply
here: this endpoint exists precisely for third parties to script against.
It joins pkg/ in the stability charter (API-STABILITY.md gets a section).
Testing & gates
Section titled “Testing & gates”- Drift gate (cli-stub-drift pattern): dump discovery from a CE and an EE binary; assert CE ⊆ EE, assert every listed route 200s/405s correctly, assert no mounted resource route is missing from discovery.
- Envelope conformance test walks every kind’s
listand validates the envelope schema. - C5 matrix: one new suite row per edition (not per cloud — the meta-API is transport-only).
Implementation sketch (post-flip)
Section titled “Implementation sketch (post-flip)”- Descriptor types + registry introspection in the API layer (CE core).
- Column/action annotations added at route-registration sites (the only hand-authored part; ~1 line per kind).
- Envelope normalization for the handful of divergent list routes.
gen-apiclient.golearns to also emit the descriptor JSON as a build artifact → the drift gate compares it against a live binary.- MCP server consumes discovery (its design doc already assumes capabilities-gating; swap in the finer contract).
Deliberately after the CE/EE flip (B7/B9): it is new public surface and must not churn the manifest/closure mid-split. Slot it with the MCP build — they share the discovery layer.
Open questions
Section titled “Open questions”- Scope filter grammar on
list(per-tenant/per-deployment query params) — adopt the existing route conventions or standardize now? - Does
columns.rendervocabulary live in the descriptor version or its own micro-version? - Should
/api/capabilitieseventually be folded into/api/resources(one probe), or stay split (cheap coarse probe vs. full contract)? - Auth hints per action (which scopes/roles) — useful for graying out UI, but leaks authz topology; include or omit?