maand serve

Run a local ops control plane: HTTP JSON API plus a simple HTML UI for managing multiple maand environments.

maand serve --root /path/to/serve-root --token "$MAAND_SERVE_TOKEN"
# optional HTTPS:
# maand serve --root /path/to/serve-root --token "$MAAND_SERVE_TOKEN" \
#   --tls-cert /path/cert.pem --tls-key /path/key.pem

Default listen address is 127.0.0.1:9090 (loopback). A bearer token is required (--token or MAAND_SERVE_TOKEN).

Open http://127.0.0.1:9090/ (or https://… when TLS PEMs are set). Use API token in the UI top bar (kept in memory for the tab and a SameSite=Strict cookie for /docs; not stored in localStorage).

curl -H "Authorization: Bearer $MAAND_SERVE_TOKEN" http://127.0.0.1:9090/api/envs

Interactive API docs (Swagger UI): http://127.0.0.1:9090/docs/ (same auth as the API)
OpenAPI 3 spec: http://127.0.0.1:9090/openapi.yaml

The Swagger UI page loads viewer assets from a CDN; /openapi.yaml is served from the binary and works offline.

Auth and TLS

Flag / env Behavior
--token / MAAND_SERVE_TOKEN Required. Sent as Authorization: Bearer … (UI also sets cookie maand_serve_token).
--addr Default 127.0.0.1:9090. Non-loopback binds (0.0.0.0, :port, …) require TLS.
--tls-cert + --tls-key Required together for HTTPS, and for any non-loopback listen address.

/api/*, /openapi.yaml, and /docs* require auth. The login shell (/, /index.html) and static UI assets (.js, .css, …) do not. Failed auth attempts are rate-limited per client IP (HTTP 429 after repeated failures).

Responses include security headers (Content-Security-Policy, X-Content-Type-Options, X-Frame-Options, Referrer-Policy; HSTS when TLS is enabled).

Serve root

--root (or MAAND_SERVE_ROOT) is a directory that contains environment buckets. It must not itself be a maand bucket (maand.conf / data/maand.db) or an env dir (env.json).

<serve-root>/
  {uuid}/
    env.json          # { "project", "name" }
    maand.conf
    workspace/
    data/
    …

Each {uuid}/ is a full maand bucket. The directory name is the env id and matches bucket_id after init.

API (prefix /api)

Full per-endpoint reference: open /docs/ on a running serve instance (or fetch /openapi.yaml with the bearer token).

Area Endpoints
Envs GET/POST/PUT/DELETE /api/envs…
Settings GET/PATCH …/settings/maand, …/settings/bucket, …/settings/job_resources/{selector}
SSH key GET/PUT/DELETE …/secrets/ssh_key
Workers GET/POST/PUT/DELETE …/workers, POST …/workers/{trust|facts|uptime|sysstat}
Ship POST …/build, …/deploy, …/health_check, …/gc, …/run_command, …/hooks{ "run_id" }
Runs GET …/runs, …/runs/{id}, long-poll …/runs/{id}/logs?after=&timeout=, cancel POST …/runs/{id}/cancel
Catalog GET …/info, …/cat/{workers|jobs|allocations|deployments|hooks|ports|kv|certs}
Jobs clone POST …/jobs, files GET …/jobs/{job}/files…, start/stop/restart

Long-running build/deploy/health/gc/hooks/run_command/worker probes/snapshot/job control: poll run status and long-poll logs.

Exclusivity and cancel

Rule Behavior
Process-wide exclusive Every async serve run is one-at-a-time across all envs in this serve process. A second start returns 409.
Per-bucket flock Each run holds data/maand.lock (advisory flock). The CLI acquires the same lock for bucket-scoped commands (init / serve excluded). Overlap → CLI error bucket busy: another maand operation is in progress, or serve 409.
Cancel POST …/runs/{runId}/cancel cancels the run context (deploy, health wait retries, job control, hooks, …). Status becomes cancelled.
Run status running · succeeded · failed · cancelled

UI

Vanilla HTML/JS embedded in the binary:

Job authoring (manifests/Makefiles) remains CLI; the UI can view job files and clone from templates/. Every folder in templates/ is treated as a job template.