HWS: API Reference

Namespaces and plugins

A namespace is the wall a tenant's deployments, secrets, tokens and auth providers sit behind. These endpoints list and declare namespaces, install per-namespace plugins such as obs, switch plugins on for the fleet, and read each namespace's event feed.

Back to the API reference.

Namespaces

Method and path Tier Crate
GET /namespaces View, narrows itself Client::namespaces() -> Vec<NamespaceEntry> · Raw::namespaces()
POST /namespaces CRUD, operator Client::create_namespace(&spec) -> Value
DELETE /namespaces/:name CRUD, operator Client::delete_namespace(name) -> ()

GET /namespaces lists the namespaces the caller can see, with the number of their deployments the caller may view. A credential confined to one namespace gets that namespace back even when it is empty.

[{"namespace": "team-a", "deployments": 4, "declared": true,
  "description": "Payments team", "created_at": 1760000000},
 {"namespace": "scratch", "deployments": 1, "declared": false}]
Field Meaning
deployments Deployments in it that this credential may see.
declared A namespace object exists, as opposed to the name being one a deployment mentions. Both scope the same way; only a declared one can be described or deleted.
description, created_at Declared namespaces only.

POST /namespaces declares one: {"name": "team-a", "description": "…"}. It answers 201 with {name, description, created_at} when new and 200 when it existed; re-declaring updates the description and keeps created_at. Declaring a namespace also installs every enabled plugin that installs automatically (see obs). A confined credential gets 403, and an invalid name 400.

DELETE /namespaces/:name undeclares one and answers 204. It is refused with 409 while deployments are still in it.

On Heyo's managed fleet, namespaces are created in Heyo cloud, not on app-lb.

Namespace plugins

Some plugins install per namespace. Two switches decide whether one does anything for a namespace: the operator enables it for the fleet (below), and a namespace administrator installs it into the namespace. Both must be on.

Method and path Tier Crate
GET /namespaces/:ns/plugins View, namespace wall Client::namespace_plugins(ns) -> Vec<NamespacePlugin> · Raw::namespace_plugins(ns)
GET /namespaces/:ns/plugins/:id View, namespace wall not in the crate
PUT /namespaces/:ns/plugins/:id CRUD, admin of the whole namespace Client::install_plugin(ns, id, config) -> NamespacePlugin
DELETE /namespaces/:ns/plugins/:id CRUD, admin of the whole namespace Client::uninstall_plugin(ns, id) -> NamespacePlugin

From namespace-plugins.json:

[
  {"id": "obs", "name": "Observability",
   "description": "Logs, metrics and alerts for every app in a namespace.",
   "enabled": true, "installed": true, "installed_at": 1760000000,
   "installed_by": "token:0123456789ab", "config": {}}
]
Field Meaning
enabled The fleet-wide switch, which the operator controls.
installed, installed_at, installed_by This namespace's install. installed_by is token:<id>, user:<id> or auto, and absent for the operator.
config The per-namespace configuration. obs takes none: send {}.

PUT installs the plugin or replaces its per-namespace config, and is idempotent. The body is {"config": {…}}, optional; the crate sends {} when config is None. DELETE uninstalls; it is allowed while the plugin is disabled. Both return the plugin as the namespace now sees it.

"Admin of the whole namespace" is the operator, a fleet admin token, a namespace token with admin scope and no deployments list (or ["*"]), or a federated namespace:<ns>:admin grant. A namespace token narrowed to particular deployments gets 403.

Status hws::Error When
400 Api Invalid namespace name, or config the plugin rejects.
403 Forbidden Not an administrator of the whole namespace.
404 NotFound No such plugin, or one that does not install per namespace.
409 Conflict The operator has the plugin switched off ("code": "plugin_disabled").

Fleet plugins

Plugins are compiled into app-lb and switched on per host. These routes are fleet-wide: a confined caller gets 403.

Method and path Tier Crate
GET /api/plugins View, operator Client::plugins() -> Vec<PluginView> · Raw::plugins()
GET /api/plugins/:id View, operator Client::plugin(id) -> PluginView
PUT /api/plugins/:id CRUD, operator Client::set_plugin(id, enabled, config) -> PluginView
GET /api/plugins/:id/installs View, operator Client::plugin_installs(id) -> PluginInstalls · Raw::plugin_installs(id)
POST /api/plugins/:id/enable, /disable CRUD, operator not in the crate; set_plugin with config: None does the same

A plugin, from plugins.json:

{"id": "example", "name": "Example", "description": "A plugin that is switched on and failing.",
 "config_schema": {"type": "object", "properties": {"url": {"type": "string"}}},
 "per_namespace": true, "installed_in": ["team-a"],
 "enabled": true, "config": {"url": "http://127.0.0.1:34199"},
 "updated_at": 1760000000, "last_error": "connection refused", "status": {"state": "retrying"}}
Field Meaning
config_schema JSON Schema for config. Advisory; app-lb validates on write.
per_namespace, installed_in Whether namespaces install it themselves, and which have.
enabled, config The stored record. config is kept while disabled.
last_error Why the last apply failed. A plugin can be enabled and failing at once.
status Whatever the plugin reports about itself.

PUT /api/plugins/:id takes {"enabled": true, "config": {…}}. With config omitted, the stored config is kept. It answers 200 once the record is saved, even if applying it failed: check last_error on the result. Invalid config is a 400.

GET /api/plugins/:id/installs lists every namespace the plugin is installed in. app-obs polls it to learn what to collect. From plugin-installs.json:

{"plugin": "obs", "enabled": true, "namespaces": ["team-a", "team-b"],
 "installs": {"team-a": {"installed_at": 1760000000, "installed_by": "user:u_123", "config": {}},
              "team-b": {"installed_at": 1760000000, "installed_by": null, "config": {}}}}

Feeds

Each namespace has a feed of lifecycle events and issues: deployments added, changed and removed, and problems such as cold-start timeouts.

Method and path Tier Crate
GET /feeds View, operator Client::feeds() -> Vec<FeedIndexEntry> · Raw::feeds()
GET /feeds/:namespace?format=json View, namespace wall Client::feed_events(ns) -> Vec<FeedEvent> · Raw::feed_events(ns)
GET /feeds/:namespace (or :namespace.xml) View, namespace wall Client::feed_rss(ns) -> String

GET /feeds lists namespaces with events: [{"namespace": "team-a", "events": 12}]. A namespace's feed is RSS by default, for feed readers; ?format=json returns the events, newest first:

{"id": 7, "ts": 1722400000, "last_ts": 1722400120, "count": 3, "namespace": "team-a",
 "deployment": "web", "kind": "issue", "title": "web: cold start timed out",
 "detail": "a request waited 120s and no VM became available"}
Field Meaning
id The RSS <guid>.
ts, last_ts, count Repeats of the same issue fold into one event: first seen, last seen, and how many times.
kind deployed, updated, removed or issue.