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:
- Projects / environments
- Workers
- Settings forms (+ SSH key, advanced raw TOML, GC)
- Catalog filters
- Run command — shell on workers (
maand run_command) with workers/labels, concurrency, health check / ignore-failure - Hooks — run
clievent hooks (maand hooks <hook> [job]) from the catalog; Activity streams output - Deployments — ship/operate deployed jobs: tabs Jobs and Rollout
- Jobs — provision/clone from
templates/with tabs Jobs (catalog table) and Templates. Clone copies intoworkspace/jobs/<name>/, then build refreshes the catalog. - Activity panel — DevTools-style logs, Cancel for the active run; UI also blocks starting a new op when a run is already
runningfor that env (cross-env still relies on API409) - API token control in the top bar
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.