Server Mode
Overview
hyve serve runs the hyve engine as a local REST + WebSocket API service — the same operations available through the CLI become available over HTTP. Every route handler is a thin wrapper around the same internal packages the CLI already calls (the same code that backs hyve cluster, hyve template, hyve workflow, etc.) — there is no separate server-only business logic.
The server is not a replacement for the CLI; both remain valid entry points. Its primary use cases are:
- A locally-hosted web UI that communicates with the running server (see Hyve Studio below)
- Any external tool that wants to call hyve as a service rather than shelling out to the CLI
- Any tooling that needs programmatic access to hyve operations without embedding Go
Quick Start
# Start the server (defaults: port 8080, repo from the registered# current repository, no auth)hyve serve
# Open a configured frontend, starting the server first if needed# (or just opening the browser if one's already running)hyve serve --openSee the hyve serve CLI reference for the full flag list.
Configuration
Add a server block to your hyve.yaml:
server: port: 8080 # default; also settable via --port or HYVE_PORT frontendUrl: "http://localhost:5173" # opened by `hyve serve --open`; appends ?server=<addr>
auth: mode: none # none (default) | forward forward: validateUrl: "" # also settable via HYVE_AUTH_VALIDATE_URL timeout: "3s" # also settable via HYVE_AUTH_VALIDATE_TIMEOUT| Field | Default | Description |
|---|---|---|
server.port | 8080 | TCP port to listen on |
server.frontendUrl | — | URL hyve serve --open navigates to; opens http://localhost:<port> directly if unset |
server.auth.mode | none | none — no auth required, binds to 127.0.0.1 only. forward — see Authentication |
server.auth.forward.validateUrl | — | External endpoint every request’s Authorization header is forwarded to for validation |
server.auth.forward.timeout | 3s | Timeout before the validator is treated as unreachable (fails closed) |
Tool Dependencies
hyve serve shells out to the same external tools the CLI does — git always, plus kubectl/helm for spec.resources, plus whatever each module you use declares in its own spec.requirements.tools (e.g. civo, aws, gcloud, jq). None of this is bundled into hyve itself — it all needs to already be on PATH inside whatever container or host runs hyve serve.
Bake them into the image. The reliable way to guarantee a tool is present is the standard one: install it in your Dockerfile (or however the runtime image is built), not at container startup. This keeps the running container reproducible and doesn’t depend on it having outbound network access, which many production deployments intentionally restrict.
FROM golang:1.23 AS build# ... build the hyve binary ...
FROM debian:bookworm-slimRUN apt-get update && apt-get install -y --no-install-recommends \ curl jq ca-certificates \ && curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" \ && install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl \ && curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash \ # add each module's own required tool here (civo, aws-cli, gcloud, k3d, ...) && rm -rf /var/lib/apt/lists/*COPY --from=build /go/bin/hyve /usr/local/bin/hyveMissing something after the image is already running? Rather than hyve installing anything itself at runtime, use an ordinary admin-triggered workflow to patch a running container — the exact same kind: Workflow mechanism used everywhere else, with script: steps that install whatever’s missing:
apiVersion: v1kind: Workflowmetadata: name: admin-install-tools description: >- Installs tools missing from this container's image. Not wired to any lifecycle hook — trigger manually: hyve workflow run admin-install-tools. Written for a Debian/Ubuntu-based image; adjust the package-manager commands for a different base image. Prefer baking these into the Dockerfile instead — treat this as a patch for a container already running, not the primary mechanism.spec: jobs: - name: install steps: - name: jq script: command -v jq >/dev/null || (apt-get update && apt-get install -y jq) - name: kubectl script: | command -v kubectl >/dev/null && exit 0 curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl - name: helm script: | command -v helm >/dev/null || curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bashREST API
All endpoints return JSON by default. Error responses follow { "error": "<message>" }. Every YAML-backed resource (clusters, templates, workflows) supports content negotiation via Accept/Content-Type: application/x-yaml for exact, byte-for-byte round-tripping of the on-disk file — this is what a “view/edit the YAML” frontend panel should use. The full machine-readable spec is served at GET /openapi.json (unauthenticated, so clients can generate typed API clients without a token).
Health and Auth Check
| Method | Path | Description |
|---|---|---|
GET | /health | Always unauthenticated. {"status":"ok","version":"..."} |
GET | /auth/check | 200 if the token is valid (or auth.mode is none), 401 otherwise. For a frontend to verify its token on page load. |
GET | /openapi.json | OpenAPI 3.1 spec, always unauthenticated |
Clusters
| Method | Path | Description |
|---|---|---|
GET | /clusters | List clusters |
GET | /clusters/{name} | Get a cluster |
POST | /clusters | Create — body: {name, template, region?, params?}; mirrors hyve cluster create |
PUT | /clusters/{name} | Full replace of the cluster YAML |
PATCH | /clusters/{name} | Partial update: pause, delete, params, expiresAt |
DELETE | /clusters/{name} | Sets spec.delete: true and triggers reconcile |
GET | /clusters/{name}/resources | spec.resources (declared) + spec.appliedResources (tracked) — read-only, no live cluster calls |
POST | /clusters/{name}/kubeconfig | Runs the module’s auth operation, returns the resulting kubeconfig YAML; mirrors hyve cluster auth |
DELETE | /clusters/{name}/kubeconfig | Removes this cluster’s entries from ~/.kube/config; mirrors hyve cluster deauth |
Templates
| Method | Path | Description |
|---|---|---|
GET | /templates | List templates |
GET | /templates/{name} | Get a template |
POST | /templates | Create — body is Template YAML or JSON |
PUT | /templates/{name} | Full replace |
DELETE | /templates/{name} | Delete |
POST | /templates/{name}/validate | Checks the driver is locked and referenced workflows exist; mirrors hyve template validate |
Workflows
| Method | Path | Description |
|---|---|---|
GET | /workflows | List workflows |
GET | /workflows/{name} | Get a workflow |
POST | /workflows | Create — body is Workflow YAML or JSON |
PUT | /workflows/{name} | Full replace |
DELETE | /workflows/{name} | Delete |
POST | /workflows/{name}/run | Body: {cluster?, inputs?} → {executionId}; mirrors hyve workflow run |
POST | /workflows/{name}/validate | Mirrors hyve workflow validate |
Remote workflow references are locked and content-addressed in hyve.lock the same way modules are. Source strings (e.g. github.com/org/repo//path@version) contain slashes, so these take the source in the request body rather than as a URL path segment:
| Method | Path | Description |
|---|---|---|
POST | /workflows/install | Resolves every remote workflow reference in templates/clusters into hyve.lock → {executionId}; mirrors hyve workflow install |
POST | /workflows/refs/update | Body: {source, path?} — re-resolves one reference to latest; mirrors hyve workflow update <source> |
GET | /workflows/refs/verify | Verifies every locked workflow’s cached content matches its sha256; mirrors hyve workflow verify |
Modules and Lock
| Method | Path | Description |
|---|---|---|
GET | /modules | List locked modules |
GET | /modules/{source}?version= | Manifest, params, requirements; mirrors hyve module info |
POST | /modules | Body: {source, version?} — locks a module; mirrors hyve module add |
POST | /modules/update | Body: {source, version} — re-resolves, refreshing its sha256; mirrors hyve module update |
POST | /modules/remove | Body: {source, version}; mirrors hyve module remove |
POST | /modules/validate | Body: {source, version}; mirrors hyve module validate |
POST | /modules/install | Locks every module referenced by templates/clusters → {executionId}; mirrors hyve module install |
POST | /modules/init | Body: {name} — scaffolds a new module skeleton; mirrors hyve module init |
GET | /lock | Parsed hyve.lock contents |
Repository Configuration
| Method | Path | Description |
|---|---|---|
GET | /config | Parsed hyve.yaml (reconcile + server + env sections) |
PATCH | /config | Partial update; writes through to hyve.yaml and commits. Changes to server.* only take effect on the next hyve serve restart. Setting env.file also updates .gitignore — see Configuration. |
Git
Scoped to the single repository the server is bound to (--path) — not the CLI’s multi-repo management surface (hyve git add/list/use/remove, branch management), which has no meaning for a server already pointed at one repo.
| Method | Path | Description |
|---|---|---|
GET | /git/status | Mirrors hyve git status for the bound repo |
POST | /git/sync | Body: {message?}; mirrors hyve git sync |
Reconcile
| Method | Path | Description |
|---|---|---|
POST | /reconcile | Body: {dryRun?} → {executionId}; mirrors hyve reconcile [--dry-run] |
POST | /reconcile/clusters/{name} | Body: {dryRun?} — reconcile a single cluster |
Executions
Long-running operations (workflow runs, reconciles, module/workflow installs) return {executionId} immediately rather than blocking the request; poll or stream to follow progress.
| Method | Path | Description |
|---|---|---|
GET | /executions | List recent executions, most recent first |
GET | /executions/{id} | Status + metadata |
GET | /executions/{id}/logs | Paginated log lines. ?since=<seq> returns only lines after that sequence number. |
GET (WS) | /executions/{id}/stream | Streams {seq, line, capturedAt} as newline-delimited JSON. Replays stored lines first so late subscribers catch up, then streams live until the execution reaches a terminal state and the server closes the connection. |
{ "seq": 1, "line": "[INFO] Applying manifests...", "capturedAt": "2026-07-05T14:32:01Z" }{ "seq": 2, "line": "[INFO] Rollout complete.", "capturedAt": "2026-07-05T14:32:04Z" }Authentication
Hyve does not validate credentials itself — it delegates the entire yes/no decision to an external HTTP endpoint you configure, and never inspects, decodes, or has any opinion about what the credential actually is (a JWT, an opaque token, an API key, whatever the calling system issues). This is the same pattern as nginx’s auth_request, Traefik’s ForwardAuth, or Envoy’s ext_authz: hyve is the enforcement point, not the decision-maker.
Every protected route passes through this middleware inline — it forwards the incoming Authorization header as-is to server.auth.forward.validateUrl and interprets the response:
- 2xx → request is valid, pass through to the handler
- Anything else (401, 403, …) → reject with
401 - Network error or timeout talking to the validator → fail closed, reject with
401
Hyve does not issue tokens, refresh them, manage users, or know anything about who validated the request beyond pass/fail — that’s entirely the calling system’s responsibility.
none mode (default — local development)
No Authorization header is required, and no validator is called. The server binds to 127.0.0.1 only and refuses to start with --host 0.0.0.0 unless --require-auth is also set — this prevents accidentally exposing an unauthenticated API on the network.
forward mode
Every request must carry whatever credential the validator expects:
Authorization: Bearer <token>Config precedence is hyve.yaml first, then environment variables:
| Env var | Corresponds to |
|---|---|
HYVE_AUTH_VALIDATE_URL | server.auth.forward.validateUrl |
HYVE_AUTH_VALIDATE_TIMEOUT | server.auth.forward.timeout |
CORS
The server allows cross-origin requests from any browser-based frontend — it reflects the request’s Origin header and answers preflight OPTIONS requests directly, since the API is bearer-token-only (never cookie-based), which makes an open CORS policy safe here.
Building a Frontend
Any web application can serve as a hyve frontend — no special client library is required, just the REST + WebSocket API above.
Connecting
When launched via hyve serve --open, the frontend receives the server address as a query parameter:
https://your-frontend.example.com?server=http://localhost:8080const params = new URLSearchParams(window.location.search)const serverUrl = params.get('server') ?? 'http://localhost:8080'Auth token
In none mode no token is needed. In forward mode, pass whatever token your validator expects — hyve forwards it verbatim without caring about its format:
https://your-frontend.example.com?server=http://localhost:8080&token=<token>const token = params.get('token') ?? ''
async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T> { const res = await fetch(`${serverUrl}${path}`, { ...options, headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), ...options.headers, }, }) if (!res.ok) throw new Error(`${res.status} ${res.statusText}`) return res.json()}Live execution output
function streamExecution(executionId: string, onLine: (line: string) => void) { const wsUrl = serverUrl.replace(/^http/, 'ws') const ws = new WebSocket(`${wsUrl}/executions/${executionId}/stream`) ws.onmessage = (e) => onLine(JSON.parse(e.data).line) return () => ws.close()}Generating a typed client
# TypeScriptnpx openapi-typescript http://localhost:8080/openapi.json -o hyve.d.ts
# Gooapi-codegen -package hyve http://localhost:8080/openapi.json > hyve_client.gen.goHyve Studio
Hyve Studio is a reference frontend for the server — a React + Tailwind app covering every endpoint above (clusters, templates, workflows, modules, git, config, reconcile, and a live executions browser). It’s a separate repository, not bundled with hyve itself.
git clone https://github.com/cbridges1/hyve-studio.gitcd hyve-studionpm installnpm run devThen, in a separate terminal:
hyve servehyve serve --open # detects the running server, opens Studio pointed at itWhat This Is Not
Not a multi-user server. hyve in server mode has no concept of users, roles, or audit trails. It runs as a single-process service under the caller’s own identity, either locally or inside a container managed by whatever deploys it. Multi-user access control, if a caller needs it, is entirely that caller’s responsibility.
Not a scheduler. The server does not poll for work or trigger reconcile on a schedule. Callers trigger reconcile explicitly — a user via hyve reconcile, an external caller via the API, or a CI/CD pipeline.
Relationship to other tools
hyve-server is deliberately consumer-agnostic. It exposes one generic REST + WebSocket API; it has no concept of, no special code path for, and no dependency on any particular external tool. A hand-rolled frontend, a script, a CI job, and a separate product are all just HTTP clients to it, indistinguishable from hyve-server’s point of view — including Hyve Studio itself, which is built entirely on top of the API documented above with no privileged integration.