Skip to content
SHC Docs

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.

Every client that renders SHC resources today hardcodes what it knows:

  • The generated pkg/apiclient knows 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 on GET /api/capabilities but 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).

  • No k8s wire compatibility. kubectl/k9s/client-go speak the exact apimachinery protocol — kubeconfig auth, /apis GVR discovery, resourceVersion watch semantics, content-negotiated Table responses, 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.

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.go walks — the descriptor is derived, never hand-maintained. Kinds without registered routes cannot appear; kinds with routes cannot hide.
  • Column hints are the k8s Table idea without the protocol: the server says how to render a list sensibly; priority > 0 marks columns a narrow client may drop. render names 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/capabilities snapshot stays the coarse feature probe (cli/client_wiring.go:75 round-trip); /api/resources is the fine-grained render contract.

/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.)

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).

ClientWhat 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/scriptslist any kind + sensible tables + live updates from day one
Future k8s-facade adapterTranslation source, if ever justified

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.

/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).

  • 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 list and validates the envelope schema.
  • C5 matrix: one new suite row per edition (not per cloud — the meta-API is transport-only).
  1. Descriptor types + registry introspection in the API layer (CE core).
  2. Column/action annotations added at route-registration sites (the only hand-authored part; ~1 line per kind).
  3. Envelope normalization for the handful of divergent list routes.
  4. gen-apiclient.go learns to also emit the descriptor JSON as a build artifact → the drift gate compares it against a live binary.
  5. 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.

  1. Scope filter grammar on list (per-tenant/per-deployment query params) — adopt the existing route conventions or standardize now?
  2. Does columns.render vocabulary live in the descriptor version or its own micro-version?
  3. Should /api/capabilities eventually be folded into /api/resources (one probe), or stay split (cheap coarse probe vs. full contract)?
  4. Auth hints per action (which scopes/roles) — useful for graying out UI, but leaks authz topology; include or omit?