Engine REST API Surface
nestr intermediate 5 min read
ELI5
The engine’s front door has a lobby (/health, /ready, /metrics, /ws handshake) and a set of rooms under /api. Rooms need a key card when the engine was started with a key reader, every room has a turnstile, and two kinds of room also count how often a free-plan organisation has used them this month.
Technical Deep Dive
Source: nestr-tools/engine internal/server/rest.go:94-170,172-200,248,282-292,486-527,529-545,600-602, internal/server/pellet_handlers.go:44-47, internal/server/usage.go:80-130, go.mod @e9180e4, read 2026-10-25. The previous revision said the router was stdlib http.ServeMux with handler-level 405s; it is gorilla/mux with .Methods(...).
Middleware order
flowchart LR REQ["request"] --> LOG["loggingMiddleware: X-Request-ID, timing"] LOG --> CORS["corsMiddleware: CORS_ALLOWED_ORIGINS"] CORS --> MUX["gorilla/mux route match"] MUX --> AUTH["auth.Middleware on /api, only if JWKS_URL set"] AUTH --> RL["withRateLimit: 10 rps, burst 20 per IP"] RL --> Q["withQuota on capped routes"] Q --> H["handler"]Routes
| Method | Path | Auth | Extra |
|---|---|---|---|
| GET | /health, /ready | none | no rate limit |
| GET | /metrics | none | Prometheus, unguarded (nestr-008) |
| GET | /ws | JWT via query or header when auth is on | event fan-out (nestr-007) |
| GET | /api/workspace, /api/services | JWT | rate limit |
| POST | /api/operations/run, /sync, /assemble | JWT | rate limit, Community cap |
| GET | /api/usage | JWT | security record, never capped |
| GET | /api/pellets, /api/pellets/{id}, /api/pellets/stats | JWT | rate limit |
| POST | /api/pellets/compress | JWT | rate limit, Community cap |
| POST | /api/pellets/extract | JWT | rate limit; verification gate, never capped |
| POST | /api/pellets/prune | JWT | rate limit |
| OPTIONS | any | none | 204 preflight |
flowchart TD API["/api subrouter"] --> J{"JWKS_URL set at start?"} J -- "no" --> OPEN["routes open, WARN logged"] J -- "yes" --> CHK["EdDSA JWT, iss and aud enforced"] CHK --> CAP{"compress or operations?"} CAP -- "yes" --> QUOTA["withQuota"] CAP -- "no" --> HAND["handler"] QUOTA --> HANDResponses
Success is the resource as JSON, or {success, message, ...} on write routes. Errors use one envelope: {"error": {"code": <http status>, "message": ..., "type": ...}, "request_id": ...} (rest.go:486-515), where request_id is the X-Request-ID the logging middleware set (inbound header, else <unixnano>-<remote addr>). Quota refusals use their own body with a string code such as E_FREE_QUOTA_EXCEEDED (nestr-025).
Key Terms
- gorilla/mux → the engine’s HTTP router; method matching via
.Methods. - withQuota → Community monthly cap middleware on compress and operations.
- CORS_ALLOWED_ORIGINS → CSV; default
http://localhost:3000,http://localhost:5173,https://app.nestr.tools.
Q&A
Q: Which router does the engine use, and what middleware wraps every route?
A: gorilla/mux. Every route passes loggingMiddleware then corsMiddleware; /api routes add the JWT middleware when enabled, the per-IP rate limiter, and on capped routes withQuota.
Q: Which routes have no auth even when JWKS_URL is set?
A: /health, /ready, /metrics and the OPTIONS preflight. /ws checks the token itself during the handshake.
Q: What does an engine error body look like?
A: {"error":{"code":<status>,"message":"...","type":"..."},"request_id":"..."}, with type derived from the status: client_error for 4xx, server_error for 5xx (rest.go:518-527).
Examples
curl -X POST https://<engine>/api/pellets/extract -H "Authorization: Bearer $JWT" -d '{"pelletId":"pid:blake3:..."}' runs the verification gate; whatever the plan, it is never refused for quota, and the outcome is recorded in the organisation’s security record. The engine’s public hostname was not confirmed for this card (UNVERIFIED).
neighbors on the map
- Two-Service Architecture onboarding to the chronicle-hq monorepo
- Request Routing & Edge Resolution debugging why a slug returns 404 instead of redirecting
- LORE Route Structure & Data Model implementing a new LORE page or route
- OSS & Cloud Modes self-hosting via docker compose