Skip to content

Admin API Reference

The Admin API is a REST interface for managing Dwaar at runtime — add and remove routes, trigger config reloads, purge cache entries, and inspect metrics without restarting the proxy.


The socket path is /var/run/dwaar-admin.sock when you pass --admin-socket without an argument. UDS connections are trusted by the OS: only processes that have read/write permission on the socket file can connect. No Authorization header is required on UDS.

Terminal window
# Start dwaar with the default UDS path
dwaar --admin-socket
# Start dwaar with a custom UDS path
dwaar --admin-socket /run/dwaar/admin.sock
# Call an endpoint over UDS
curl --unix-socket /var/run/dwaar-admin.sock http://localhost/health
curl --unix-socket /var/run/dwaar-admin.sock http://localhost/routes

Worker 0 always binds TCP on 127.0.0.1:6190. This interface requires a bearer token on every authenticated request.

Terminal window
# Health — no token needed
curl http://127.0.0.1:6190/health
# Authenticated request
curl -H "Authorization: Bearer $DWAAR_ADMIN_TOKEN" \
http://127.0.0.1:6190/routes

TCP connections require a bearer token. Set the token via the environment variable DWAAR_ADMIN_TOKEN before starting Dwaar:

Terminal window
export DWAAR_ADMIN_TOKEN="$(openssl rand -hex 32)"
dwaar

If DWAAR_ADMIN_TOKEN is not set, Dwaar starts but rejects all TCP requests with 401. The warning admin API will reject all authenticated requests appears in the log.

Include the token in every TCP request:

Authorization: Bearer <token>

Unix socket connections bypass token authentication — access is controlled by the socket file’s filesystem permissions (mode 0600, owner = the Dwaar process user).

The admin API is not designed for browser cross-origin use. As of 0.2.3:

  • Every response sets Access-Control-Allow-Origin: null — no origin is ever permitted.
  • OPTIONS preflight returns 405 Method Not Allowed with an Allow: header listing only the real methods the endpoint accepts.

If you had a browser-based management UI pointing directly at the admin API, it will fail with a CORS error. This is intentional. Route browser traffic through a dedicated backend service that holds the admin token, or proxy the admin endpoint from a same-origin path inside your application so the browser never sees the cross-origin response.

Unix socket and curl-style clients are unaffected — they do not enforce the browser same-origin policy.

Authenticated requests are subject to a global rate limit of 60 requests per 60-second window. Exceeding the limit returns 429 Too Many Requests. The GET /health endpoint is exempt.


Returns proxy liveness and uptime. No authentication required on either transport.

Response 200 OK

{
"status": "ok",
"uptime_secs": 3742
}
FieldTypeDescription
statusstringAlways "ok" when the process is alive
uptime_secsintegerSeconds since process start

List all active routes in the route table.

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
http://127.0.0.1:6190/routes

Response 200 OK

[
{
"domain": "api.example.com",
"upstream": "10.0.0.5:8080",
"tls": false,
"rate_limit_rps": 500,
"under_attack": false,
"source": null
},
{
"domain": "www.example.com",
"upstream": "10.0.0.6:443",
"tls": true,
"rate_limit_rps": null,
"under_attack": false,
"source": "dwaar-ingress"
}
]
FieldTypeDescription
domainstringHostname pattern (lowercase). Wildcard form: *.example.com
upstreamstring|nullUpstream socket address, or null for file-server-only routes
tlsbooleanWhether the proxy connects to the upstream over TLS
rate_limit_rpsinteger|nullPer-IP request rate limit, or null if not set
under_attackbooleanChallenge mode active for this route
sourcestring|nullController that owns this route (e.g. "dwaar-ingress"), or null

Status codes

CodeMeaning
200Route list returned
401Missing or invalid bearer token (TCP only)
429Rate limit exceeded
500Internal serialization error

Add a new route or replace an existing one with the same domain. The domain key is compared case-insensitively.

Terminal window
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"domain":"app.example.com","upstream":"10.0.1.10:8080","tls":false}' \
http://127.0.0.1:6190/routes

Request body

{
"domain": "app.example.com",
"upstream": "10.0.1.10:8080",
"tls": false,
"source": "my-controller"
}
FieldTypeRequiredDescription
domainstringyesHostname to route. Wildcards accepted: *.example.com
upstreamstringyesSocket address in host:port form
tlsbooleanyesConnect to upstream with TLS
sourcestringnoController identity tag for ownership tracking

Response 201 Created

{
"domain": "app.example.com",
"upstream": "10.0.1.10:8080",
"tls": false,
"rate_limit_rps": null,
"under_attack": false,
"source": "my-controller"
}

Status codes

CodeMeaning
201Route created or replaced
400Invalid JSON, invalid domain, or invalid upstream address
401Missing or invalid bearer token (TCP only)
413Request body exceeds 64 KB
429Rate limit exceeded

Remove a route by domain. The domain is matched case-insensitively.

Terminal window
curl -X DELETE \
-H "Authorization: Bearer $TOKEN" \
http://127.0.0.1:6190/routes/app.example.com

Response 200 OK

{
"deleted": "app.example.com"
}

Status codes

CodeMeaning
200Route deleted; body contains the deleted domain
400Domain segment is empty
401Missing or invalid bearer token (TCP only)
404No route with that domain exists
414Domain segment longer than 253 bytes (RFC 1035 max). Rejected before any to_lowercase() allocation. Added in 0.2.3.
429Rate limit exceeded

Replace the complete route set for one controller source. Routes owned by other sources are left untouched.

Terminal window
curl -X PUT \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"source":"permanu-agent","routes":[{"domain":"app.example.com","upstream":"10.0.1.10:8080","tls":false}]}' \
http://127.0.0.1:6190/routes/snapshot

Request body

{
"source": "permanu-agent",
"routes": [
{
"domain": "app.example.com",
"upstream": "10.0.1.10:8080",
"tls": false
}
]
}
FieldTypeRequiredDescription
sourcestringyesController identity that owns this desired route set
routesarraynoComplete desired route list for source; omitted or empty removes all routes owned by source
routes[].domainstringyesHostname to route. Wildcards accepted: *.example.com
routes[].upstreamstringyesSocket address in host:port form
routes[].tlsbooleanyesConnect to upstream with TLS

Response 200 OK

{
"source": "permanu-agent",
"applied": 1,
"removed": 0,
"total_routes": 4,
"route_hash": "b7f3..."
}

route_hash is stable for the same source and desired route set regardless of input order. Controllers can store it as the applied-state marker.

Status codes

CodeMeaning
200Snapshot applied
400Invalid JSON, source, domain, upstream, or duplicate route domain
401Missing or invalid bearer token (TCP only)
413Request body exceeds 64 KB
429Rate limit exceeded

Serve Prometheus metrics in text exposition format (text/plain; version=0.0.4). Requires Prometheus support to be enabled at startup (enabled by default; disable with --no-metrics).

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
http://127.0.0.1:6190/metrics

Response 200 OK — Prometheus text format

# HELP dwaar_requests_total Total requests proxied
# TYPE dwaar_requests_total counter
dwaar_requests_total{domain="api.example.com",status="2xx"} 148203
...

Status codes

CodeMeaning
200Metrics text returned
401Missing or invalid bearer token (TCP only)
404Metrics not enabled; start without --no-metrics=false
429Rate limit exceeded

Return analytics snapshots for all tracked domains as a JSON array. Domains with no traffic since startup are not included.

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
http://127.0.0.1:6190/analytics

Response 200 OK

[
{
"domain": "www.example.com",
"page_views_1m": 84,
"page_views_60m": 3902,
...
}
]

See Analytics API for the complete response schema.

Status codes

CodeMeaning
200Array of domain snapshots (empty array if no data)
401Missing or invalid bearer token (TCP only)
429Rate limit exceeded
500Internal serialization error

Return the analytics snapshot for a single domain.

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
http://127.0.0.1:6190/analytics/www.example.com

Response 200 OK — see Analytics API for the full schema.

Status codes

CodeMeaning
200Domain snapshot returned
400Domain segment is empty or contains invalid characters
401Missing or invalid bearer token (TCP only)
404No analytics recorded for this domain
429Rate limit exceeded

Invalidate a single cache entry. The key is derived from the host and path segments of the URL. Requires cache storage to be enabled.

Terminal window
curl -X PURGE \
-H "Authorization: Bearer $TOKEN" \
"http://127.0.0.1:6190/cache/www.example.com/blog/post-slug"

The key format matches what the proxy stores: {host}/{path} where path begins with /. Leading / is added automatically if absent.

Response 200 OK — entry was found and invalidated

{ "purged": true }

Response 404 Not Found — entry was not in the cache

{ "purged": false, "reason": "not found" }

Status codes

CodeMeaning
200Cache entry invalidated
400Key segment is empty
401Missing or invalid bearer token (TCP only)
404Entry not found in cache
429Rate limit exceeded
501Cache not enabled

Signal Dwaar to re-read the Dwaarfile and atomically swap the route table. Requires the config watcher to be active (default when a Dwaarfile exists). A cooldown of 5 seconds is enforced between consecutive reloads.

Terminal window
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
http://127.0.0.1:6190/reload
# From the CLI (wraps this endpoint)
dwaar reload --admin 127.0.0.1:6190

Response 200 OK

{ "message": "config reload triggered" }

Response 400 Bad Request — parse error in the new Dwaarfile

As of 0.2.2, a failed parse returns 400 with the full ConfigError::Display output as the response body. Content-Type is text/plain; charset=utf-8 so the error reads cleanly when piped to a terminal. The running config is never touched on parse failure — the cooldown is still consumed so a broken file cannot be hot-reloaded in a tight loop.

Terminal window
curl -sS -X POST \
-H "Authorization: Bearer $TOKEN" \
http://127.0.0.1:6190/reload
parse error at line 12 col 5: unexpected token 'reverse_proxys'
expected one of: reverse_proxy, respond, handle, handle_path, route, ...
did you mean 'reverse_proxy'?

Response 429 Too Many Requests — cooldown not elapsed

{ "error": "reload too soon", "retry_after": 3 }

The Retry-After response header contains the same integer value as retry_after.

Status codes

CodeMeaning
200Reload signal sent to config watcher
400Parse error in the new Dwaarfile. Body is the full error text (plain). Running config is unchanged.
401Missing or invalid bearer token (TCP only)
429Cooldown period active; see Retry-After header
501Config watcher not active

All error responses use a consistent JSON envelope:

{ "error": "<human-readable message>" }

The Content-Type is always application/json. The message is safe to display — special characters are escaped via serde_json.

Common errors

Statuserror valueCause
400"invalid JSON: ..."Malformed request body
400"invalid domain: ..."Domain fails validation
400"invalid upstream address: ..."Not a valid host:port
400"missing domain"Empty path segment in DELETE
401"unauthorized"Bearer token absent or wrong
405"method not allowed"Wrong HTTP method for this path; check Allow header
413"request body too large"Body exceeds 64 KB
429"rate limit exceeded"Global 60 req/60 s window
500"serialize error: ..."Internal failure serializing the response
501"reload not supported — config watcher not active"Reload called without watcher
501"cache not enabled"PURGE called without cache backend

Mutating operations emit a structured tracing::info! event at target dwaar::admin::audit. The following operations produce an audit event:

Operationaction valueresource value
POST /routesroute_adddomain name
DELETE /routes/{domain}route_deletedomain name
PURGE /cache/{host}/{path}cache_purge{host}/{path} key

Every audit event includes the fields action, principal (always "admin" for API-driven mutations), and resource.

To capture only audit events, set:

RUST_LOG=dwaar::admin::audit=info

This lets you route audit entries to a separate log sink without increasing the overall log verbosity. Audit events are INFO level and flow through the normal tracing subscriber — they appear in whatever output format your subscriber is configured to use.