Lunar - Integration
Lunar hosts multiple services. Each service is enabled independently in config, and a single Lunar instance can run one, several or all of them at once. Today the subscriber service (a RADIUS server) is available. More services will be added over time.
Lunar exposes a small HTTP/JSON control API. It is how an operator (or an upstream backend API) inspects a running Lunar instance and drives whatever services it is running. Such a backend API is the system that manages instances on Lunar. For example, the NebularStack Subscriber Management module, which manages Lunar and its subscriber (RADIUS) service.
This document describes the top-level, service-agnostic API. The routes that belong to the Lunar process itself rather than to any one service. The only top-level route today is GET /v1/status. Everything a specific service adds is documented alongside that service, never here, so that this document stays independent of which services exist:
Subscriber (RADIUS) API. See the RADIUS Integration guide.
Where the API listens
The API server is a single embedded HTTP server on every lunar instance. It is shared by every enabled service (each service attaches its own routes to it), so there is one listener regardless of how many services are running. Its listen address and worker pool come from config:
- lunar.api_host
The bind address. Default 127.0.0.1, which is loopback only, reachable from the same host (or over an SSH tunnel or private overlay). Set it to a routable address only behind a trusted network boundary.
- lunar.api_port
The TCP port. Default 1337.
- lunar.api_threads
The number of HTTP worker threads. Default 4.
All routes are versioned under /v1 and speak JSON.
Authentication
Every route is gated by a single shared secret. The caller must send it in the x-secret request header:
x-secret: <the value of lunar.secret>
The value is whatever lunar.secret is set to in config. It is compared in constant time, so a wrong or missing secret is rejected with 401 Unauthorized and no body. The same secret gates every route, top-level and service. There is no other authentication scheme (no cookies, no bearer tokens). Protect the API by keeping the listener private and the secret secret.
GET /v1/status
A core health + vitals endpoint and it is service-agnostic. It reports this host's vitals, probes whatever backend health checks the enabled services registered at startup, and reports the instance's license health. Each service that needs a backend to be reachable contributes its own check. The route itself knows nothing about any particular service.
Use it as a load-balancer / monitoring probe and as a quick "is this instance healthy and licensed?" check. It always returns 200 OK when the secret is valid, whether the instance is healthy or not: the body's status field carries the verdict (ok or bad), so poll the JSON, not the HTTP status code, for health. When status is bad the same object also carries an errors array (one human-readable string per failure), only present when non-empty:
{
"status": "bad",
"hostname": "lunar-1.domain.com",
"...": "...",
"errors": [
"<service>: https://backend.example.com/health failed: Connection timed out after 5000 ms",
"license: license expired"
]
}
Instance Health and Vitals.
lunar
What "healthy" means
The verdict combines two things:
Backend health checks. Each enabled service registers zero or more backend endpoints that must answer 200 OK. Each is probed with a 5 second timeout. Anything other than a 200 (a non-200 status, a timeout, a DNS or connection failure) is one entry in errors, prefixed with the service it belongs to. Which checks exist depends entirely on which services are enabled and is documented with each service.
License health. A missing, invalid or expired-past-grace license is a single license: error (no per-service license lines are then added). Only when the license is otherwise valid is each enabled service checked for coverage (for example that subscriber is licensed), and any gap is its own license: line.