# Admin API

URL: https://scalws.com/docs/reference/admin-api/
Source: `docs/reference/admin-api.md` in the ScalWS repository

`scalwsd` serves one admin API, HTTP/1.1 with JSON or plain-text answers, on up to four
transports. `scalwsctl` and the console use it; this page lists every endpoint for tools
that call it directly. Design records: [ADR-0017](https://scalws.com/docs/adr/0017-admin-api-and-operations/)
(socket and views), [ADR-0023](https://scalws.com/docs/adr/0023-remote-admin-mtls/) (remote admin) and
[ADR-0049](https://scalws.com/docs/adr/0049-upstream-pools-and-runtime-members/) (upstream pools).

## Transports

| Transport | Configured by | Access | Serves |
|---|---|---|---|
| Admin socket | [`server.admin.socket`](https://scalws.com/docs/guide/configuration/#admin), default `/run/scalws/admin.sock` | Unix socket, mode `0660`: the server's user and group. Each change is audited with the peer's uid, gid and pid. | Every endpoint. |
| Remote admin | [`server.admin.remote`](https://scalws.com/docs/guide/configuration/#admin) | TCP with mutual TLS: only clients with a certificate from `client_ca`. Each change is audited with the certificate subject and the client address. | Every endpoint, like the socket. |
| Metrics listener | [`server.metrics.address`](https://scalws.com/docs/guide/configuration/#metrics) | Loopback TCP, no authentication. | `GET` of the operational views marked "metrics" below. Changes answer `403`; other views answer `403` too. |
| Probe listener | `server.health.address` | TCP, no authentication. | `GET /healthz`, `GET /readyz`, and `GET /metrics` with `server.health.metrics: true`; everything else is `404`. |

Paths may carry a `/v1` prefix (`/v1/status` and `/status` are the same endpoint); tools
should use it. Changes use `POST` (or `PUT` for upstream members); other methods answer
`405`, unknown paths `404`. Error answers are one line of plain text.

Over the socket with curl:

```sh
curl --unix-socket /run/scalws/admin.sock http://localhost/v1/status
```

## Views

All views are `GET`. "Metrics" marks the views the metrics listener serves as well.

| Path | Metrics | Answer |
|---|---|---|
| `/v1/metrics` | yes | Prometheus text format, with the per-domain counters. See [logging-metrics.md](https://scalws.com/docs/guide/logging-metrics/). |
| `/v1/healthz` | yes | `200 ok`, or `503 draining` while the process shuts down. |
| `/v1/readyz` | yes | `{"state": "ready" \| "active" \| "draining", "generation", "instance", "pid"}`; `503` while draining. |
| `/v1/status` | yes | Version, generation, uptime, listeners, metrics address, admin socket, counts of tenants and applications, enabled subsystems, draining. |
| `/v1/apps` | yes | One entry per application: domains, runtime type, state, workers, requests in flight, recent requests and p95, diagnostic findings. |
| `/v1/diagnose?app=tenant/app` | yes | Diagnostics of every application, or of one; `404` for an unknown application or before the first window. |
| `/v1/optimizer` | yes | The optimizer journal. |
| `/v1/explain?app=tenant/app` | yes | The last advice of the local advisor; `400` without `app`, `404` without advice, `503` when `server.ai` is off. |
| `/v1/dashboard`, `/` | yes | The read-only HTML dashboard. |
| `/v1/tenants` | no | Tenants with their user, applications, resources and, with cgroups, memory and CPU usage. |
| `/v1/tenants/usage?interval_ms=1000` | no | CPU, memory, I/O and pressure of each tenant over the interval (200 to 10,000 ms), with its limits; `409` without cgroup v2. |
| `/v1/config/history` | no | The kept configuration generations with time and source. |
| `/v1/config/diff?from=N&to=M` | no | Line diff of two kept generations (plain text); `404` for a generation that is not kept. |
| `/v1/certs` | no | ACME certificate status per application. |
| `/v1/upstreams` | no | Every upstream pool with its members ([below](#upstream-pools)). |
| `/v1/upstreams/{name}` | no | One pool; `404 unknown pool`. |

## Changes

Available on the admin socket and the remote admin listener only.

| Method and path | Effect | Answers |
|---|---|---|
| `POST /v1/reload` | Reads the configuration file and applies it. | `200 {"generation": N}`; `422` with the reason when it is refused. |
| `POST /v1/rollback?generation=N` | Applies a kept generation again (default: the previous one). | `200 {"generation": N}`; `400` for a non-numeric generation; `409` with the reason. |
| `POST /v1/apps/{tenant}/{app}/restart` | Starts a fresh instance of the application's runtime, then drains the old one. | `200 {"generation": N}`; `409` with the reason. |
| `POST /v1/apps/{tenant}/{app}/scale?workers=N` | Sets the worker target of a managed Node.js pool within its bounds. | `200 {"from", "to", "min", "max"}`; `400` without `workers`; `404` unknown application; `409` for a runtime that cannot be scaled. |
| `POST /v1/cache/purge?host=H&path=P` (or `host` and `prefix`, or `tag`, or `all=1`) | Removes cached responses. | `200 purged`; `400` for another combination; `404 cache disabled`. |
| `POST /v1/explain?app=tenant/app` | Queues a request to the local advisor. | `202 queued`; `400` without `app`; `404` unknown application; `429` when the queue is full; `503` when `server.ai` is off. |
| `POST /v1/tenants/{tenant}/limits?cpu=&memory=&memory_percent=&io_weight=&io_bandwidth=&clear=` | Sets (or with `clear=cpu,memory,io_weight,io_bandwidth` or `all` removes) run-time cgroup limits of a tenant, kept until scalwsd restarts. | `200` with the override and the effective limits; `400` invalid value; `404` unknown tenant; `409` without cgroup v2; `500` when the kernel refused it. |
| `PUT /v1/upstreams/{name}` | Replaces the members of a `members_from: api` pool ([below](#put-v1upstreamsname)). | `200`, `400`, `404`, `409`, `413`, `501`. |

## Upstream pools

### `GET /v1/upstreams`

Every pool of the active configuration, sorted by name:

```json
{
  "pools": [
    {
      "name": "api",
      "members_from": "api",
      "balance": "round_robin",
      "members": [
        {
          "address": "192.0.2.5:8080",
          "weight": 2,
          "active_requests": 1,
          "consecutive_failures": 0,
          "ejected_until_unix_ms": null
        },
        {
          "address": "192.0.2.6:8080",
          "weight": 1,
          "active_requests": 0,
          "consecutive_failures": 0,
          "ejected_until_unix_ms": 1792000000000
        }
      ]
    }
  ]
}
```

| Field | Meaning |
|---|---|
| `name` | Pool name (`upstreams.<name>`). |
| `members_from` | `file` or `api`. |
| `balance` | `round_robin`, `least_conn` or `hash`. |
| `members[].address` | Canonical member address: `host:port`, `[ipv6]:port` or `unix:/path`. |
| `members[].weight` | Weight, 1 to 1000. |
| `members[].active_requests` | Requests in flight on the member (until their response body is finished). |
| `members[].consecutive_failures` | Failures since the last success or ejection. |
| `members[].ejected_until_unix_ms` | End of the current ejection in Unix milliseconds; `null` while the member takes traffic. |

With no pools the answer is `{"pools": []}`.

### `GET /v1/upstreams/{name}`

One pool, as one element of `pools` above. `404 unknown pool` for a name that is not
configured.

### `PUT /v1/upstreams/{name}`

Replaces the whole member list of a pool with `members_from: api`, without a reload:

```sh
curl --unix-socket /run/scalws/admin.sock -X PUT \
  --data '{"members": [{"address": "192.0.2.5:8080", "weight": 2}, {"address": "192.0.2.6:8080"}]}' \
  http://localhost/v1/upstreams/api
```

The body is a JSON object with exactly one key, `members`: a list of
`{"address": "...", "weight": N}`, where `weight` is optional (default 1). Addresses follow
the configuration's rules: `ip:port`, `[ipv6]:port`, `host:port`, or `unix:/path` as the
only member of a plain-HTTP pool. An empty list empties the pool (its requests then get
`503` with `Retry-After: 1`).

| Status | Body | When |
|---|---|---|
| `200` | The pool, as in `GET /v1/upstreams/{name}` | The list was installed. |
| `400` | `invalid body: ...` or the first problem, for example `members[1].weight: must be between 1 and 1000` | Not a valid body; an invalid address or weight; a duplicate member; more than 10,000 members. |
| `404` | `unknown pool` | No pool of that name. |
| `409` | `the pool takes its members from the configuration file (members_from: file)` | A file pool. |
| `413` | `body larger than 2 MiB` | The body is too large. |
| `501` | `run-time pool members are not supported under scalws-controller yet` | The process is an instance of scalws-controller (`listener_mode: inherited` or `reuseport`). |
| `403` | | Sent to the metrics listener. |

A refused request changes nothing. The change is serialised with reloads; members that stay
(same canonical address) keep their connections, load and health, requests in flight to a
removed member finish, and the series of removed members leave the metrics. Members set
this way survive reloads but not a restart. See
[load-balancing.md](https://scalws.com/docs/guide/load-balancing/#changing-members-at-run-time).

`scalwsctl upstreams`, `scalwsctl upstreams show <pool>` and
`scalwsctl upstreams set <pool> <addr[=weight]>...` call these endpoints
([scalwsctl](https://scalws.com/docs/reference/programs/scalwsctl/#upstreams)).

### Under scalws-controller

The controller's local control API (`controller.socket`, default
`/run/scalws-controller/control.sock`, mode `0600`; see
[adaptive-runtime.md](https://scalws.com/docs/adaptive-runtime/)) answers the same three calls without the
`/v1` prefix:

| Method and path | Answer |
|---|---|
| `GET /upstreams`, `GET /upstreams/{name}` | Forwarded to the admin socket of one serving instance: `200 {"ok": true, "result": <the instance's answer>}`, or `502 {"ok": false, ...}` with the instance's status and answer, the connection error, or `no serving instance`. |
| `PUT /upstreams/{name}` | `501 {"error": "run-time pool members are not supported under scalws-controller yet; use members_from: file"}`. |

A pool name that does not follow the name rule (lower-case letter first, then `a-z`, `0-9`,
`-`, `_`, at most 63 characters) is `404` there.
