Skip to content
Docs

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

Terminal window
# 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 --open

See 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
FieldDefaultDescription
server.port8080TCP port to listen on
server.frontendUrlURL hyve serve --open navigates to; opens http://localhost:<port> directly if unset
server.auth.modenonenone — no auth required, binds to 127.0.0.1 only. forward — see Authentication
server.auth.forward.validateUrlExternal endpoint every request’s Authorization header is forwarded to for validation
server.auth.forward.timeout3sTimeout 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-slim
RUN 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/hyve

Missing 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:

workflows/admin-install-tools.yaml
apiVersion: v1
kind: Workflow
metadata:
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 | bash

REST 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

MethodPathDescription
GET/healthAlways unauthenticated. {"status":"ok","version":"..."}
GET/auth/check200 if the token is valid (or auth.mode is none), 401 otherwise. For a frontend to verify its token on page load.
GET/openapi.jsonOpenAPI 3.1 spec, always unauthenticated

Clusters

MethodPathDescription
GET/clustersList clusters
GET/clusters/{name}Get a cluster
POST/clustersCreate — 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}/resourcesspec.resources (declared) + spec.appliedResources (tracked) — read-only, no live cluster calls
POST/clusters/{name}/kubeconfigRuns the module’s auth operation, returns the resulting kubeconfig YAML; mirrors hyve cluster auth
DELETE/clusters/{name}/kubeconfigRemoves this cluster’s entries from ~/.kube/config; mirrors hyve cluster deauth

Templates

MethodPathDescription
GET/templatesList templates
GET/templates/{name}Get a template
POST/templatesCreate — body is Template YAML or JSON
PUT/templates/{name}Full replace
DELETE/templates/{name}Delete
POST/templates/{name}/validateChecks the driver is locked and referenced workflows exist; mirrors hyve template validate

Workflows

MethodPathDescription
GET/workflowsList workflows
GET/workflows/{name}Get a workflow
POST/workflowsCreate — body is Workflow YAML or JSON
PUT/workflows/{name}Full replace
DELETE/workflows/{name}Delete
POST/workflows/{name}/runBody: {cluster?, inputs?}{executionId}; mirrors hyve workflow run
POST/workflows/{name}/validateMirrors 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:

MethodPathDescription
POST/workflows/installResolves every remote workflow reference in templates/clusters into hyve.lock{executionId}; mirrors hyve workflow install
POST/workflows/refs/updateBody: {source, path?} — re-resolves one reference to latest; mirrors hyve workflow update <source>
GET/workflows/refs/verifyVerifies every locked workflow’s cached content matches its sha256; mirrors hyve workflow verify

Modules and Lock

MethodPathDescription
GET/modulesList locked modules
GET/modules/{source}?version=Manifest, params, requirements; mirrors hyve module info
POST/modulesBody: {source, version?} — locks a module; mirrors hyve module add
POST/modules/updateBody: {source, version} — re-resolves, refreshing its sha256; mirrors hyve module update
POST/modules/removeBody: {source, version}; mirrors hyve module remove
POST/modules/validateBody: {source, version}; mirrors hyve module validate
POST/modules/installLocks every module referenced by templates/clusters → {executionId}; mirrors hyve module install
POST/modules/initBody: {name} — scaffolds a new module skeleton; mirrors hyve module init
GET/lockParsed hyve.lock contents

Repository Configuration

MethodPathDescription
GET/configParsed hyve.yaml (reconcile + server + env sections)
PATCH/configPartial 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.

MethodPathDescription
GET/git/statusMirrors hyve git status for the bound repo
POST/git/syncBody: {message?}; mirrors hyve git sync

Reconcile

MethodPathDescription
POST/reconcileBody: {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.

MethodPathDescription
GET/executionsList recent executions, most recent first
GET/executions/{id}Status + metadata
GET/executions/{id}/logsPaginated log lines. ?since=<seq> returns only lines after that sequence number.
GET (WS)/executions/{id}/streamStreams {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 varCorresponds to
HYVE_AUTH_VALIDATE_URLserver.auth.forward.validateUrl
HYVE_AUTH_VALIDATE_TIMEOUTserver.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:8080
const 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

Terminal window
# TypeScript
npx openapi-typescript http://localhost:8080/openapi.json -o hyve.d.ts
# Go
oapi-codegen -package hyve http://localhost:8080/openapi.json > hyve_client.gen.go

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

Terminal window
git clone https://github.com/cbridges1/hyve-studio.git
cd hyve-studio
npm install
npm run dev

Then, in a separate terminal:

Terminal window
hyve serve
hyve serve --open # detects the running server, opens Studio pointed at it

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