Skip to content

HTTP API

AetherEdge runs six HTTP services, but remote applications have one network boundary: aether-api on port 6005. This page describes the gateway, its service-local contracts, and the security conventions that apply across it.

The OpenAPI document generated by each running service is the sole source of truth for its paths, parameters, request and response schemas, status codes, media types, and operation-specific security requirements. Use the matching Swagger UI to explore and call those operations.

Service Default boundary Swagger UI OpenAPI JSON
aether-io loopback http://127.0.0.1:6001/docs http://127.0.0.1:6001/openapi.json
aether-automation loopback http://127.0.0.1:6002/docs http://127.0.0.1:6002/openapi.json
aether-history loopback http://127.0.0.1:6004/docs http://127.0.0.1:6004/openapi.json
aether-api remote gateway http://<edge-host>:6005/docs http://<edge-host>:6005/openapi.json
aether-uplink loopback http://127.0.0.1:6006/docs http://127.0.0.1:6006/openapi.json
aether-alarm loopback http://127.0.0.1:6007/docs http://127.0.0.1:6007/openapi.json

Swagger is opt-in through each service’s swagger-ui Cargo feature. To include it in all six services in an installer build, use:

Terminal window
./scripts/build-installer.sh <version> <arch> -s rust --enable-swagger

When enabled, /docs and /openapi.json are public routes. Swagger does not bypass authentication on protected operations, but its schemas and the otherwise-internal API surface are visible. Enable it only on a trusted commissioning or development network.

Only aether-api is a remote-facing service. Its authenticated application gateway exposes five fixed namespaces and forwards them only to configured loopback services:

Remote namespace Service-local owner
/api/v1/io/* aether-io
/api/v1/automation/* aether-automation
/api/v1/history/* aether-history
/api/v1/uplink/* aether-uplink
/api/v1/alarm/* aether-alarm

The target is selected by the namespace, never by caller input. Startup validation accepts only explicit loopback HTTP origins. The gateway preserves the original signed Bearer token and the small set of documented command and conditional-request headers; it discards caller-supplied actor headers and sanitizes upstream transport errors. The direct service ports remain internal and must not be published from the device.

Generated applications and downstream product interfaces use the corresponding gateway-prefixed path on port 6005. Service-local OpenAPI remains the exact operation contract during the current consolidation stage; clients translate its internal path through the fixed namespace above. This does not make the service-local port a supported client interface. A missing operation still has to be added through the owning application boundary rather than invented by a UI, attached directly to SHM, or implemented as a storage write.

Loopback is a deployment boundary, not an identity credential. IO channel commissioning plus selected automation and alarm commands authenticate at the operation boundary, but many other local management routes in io, history, uplink, automation, and alarm still rely on host isolation. Do not infer that a direct service port is safe to expose because some of its operations declare a Bearer scheme.

The current full channel-configuration query can include protocol parameters and per-channel logging configuration. It remains compatibility debt pending a redacted, authenticated application query capability. Keep the io port on loopback and do not proxy that response to an untrusted client.

aether-api protects its management routes with a signed access JWT. REST clients send it only in the standard header:

Authorization: Bearer <access-token>

Login, refresh-token lifecycle endpoints, service health, and—when compiled in—the documentation routes form the public transport surface described by the gateway OpenAPI document. Public registration is disabled unless explicitly enabled. The required JWT_SECRET_KEY must contain at least 32 bytes and must be managed outside source control.

The gateway WebSocket is also authenticated. Its documented ?token=... fallback is accepted only for an actual WebSocket upgrade; REST query-string tokens are rejected. WebSocket control writes are not supported.

The gateway requires an access JWT before forwarding any namespace request. The owning service then applies operation-specific authorization:

  • io channel create, update, delete, enable, and disable require an Admin or Engineer Bearer JWT with io.channel.manage;
  • automation device actions accept a Bearer access JWT or the dedicated AetherService <token> uplink credential;
  • automation rule management and manual execution require an Admin or Engineer Bearer JWT with the capability documented for the operation;
  • alarm rule mutation and alert resolution require an Admin or Engineer Bearer JWT;
  • forwarded identity headers and loopback reachability do not satisfy these protected command boundaries.

Commands that can change channel configuration, device, rule, processing, or alarm state declare their risk, permission, idempotency, confirmation, and audit policy in OpenAPI. The application command boundary enforces those declarations; the HTTP handler must not write SHM or storage directly.

For a protected mutation, follow the operation schema exactly. Depending on the operation, explicit confirmation is carried as x-aether-confirmed: true or in the request body. Supply x-request-id when documented so retries and audit records share a stable correlation ID. Never assume that confirmation or identity may be forwarded in an undeclared header.

An accepted governed command includes its request_id and audit outcome in the response described by OpenAPI. If dispatch or persistence succeeded but the terminal audit append failed, the operation is still accepted and its audit state is marked incomplete and non-retryable. Retain the correlation ID and do not automatically submit the command again. Failure to record the attempted audit fails closed before dispatch.

Device-command acceptance means the local command plane accepted the request; it is not proof that physical equipment executed it. Use feedback telemetry for closed-loop confirmation.

For channel commissioning, SQLite desired configuration and the active protocol runtime are deliberately distinct. Existing-resource mutations may document an optional x-aether-expected-revision compare-and-set guard. An accepted response can report an activation-pending or degraded runtime projection after desired state has committed; reconcile by request_id and resulting_revision rather than automatically repeating the non-idempotent mutation. The exact headers, receipt fields, and per-operation status codes are defined by the io Swagger document.

Most business handlers return the shared success envelope:

{ "success": true, "data": { "...": "..." }, "metadata": { "...": "..." } }

metadata is omitted when empty. Health probes, service banners, WebSocket upgrades, CSV exports, and strict Data Processing responses intentionally use their own representations.

Error responses are still migrating and may use one of these compatibility shapes:

  • { "success": false, "error": { "code": 400, "message": "..." } };
  • { "success": false, "message": "..." };
  • the versioned Data Processing { "error": { "code": "...", ... } } form;
  • the flat AetherError mapping with error_code, category, and retryable.

Clients must treat the operation’s OpenAPI status and response content type as authoritative and tolerate the documented compatibility shape. Do not infer a universal error schema from another service.

When a route, schema, security rule, response, or feature gate changes, update the owning service’s generated OpenAPI annotations and tests in the same change. Remote examples must use the gateway-prefixed form while local contract tests may use the owning loopback service. Do not add a second endpoint list to Markdown. Run the six-service parity check:

Terminal window
./scripts/check-openapi-contracts.sh

This check compiles the opt-in Swagger feature and verifies each service-owned Router/OpenAPI contract. It also runs in the Rust CI workflow.