openapi: 3.0.3 info: title: Raahi Admin API version: 0.5.1 description: | Control plane for Raahi's reverse-proxy configuration and runtime state. All configuration mutations are persisted and hot-reloaded before the response is returned. PUT operations are full replacements, not patches. Successful creates and deletes return HTTP 200. Authentication is open until an admin token is generated or a user is created. Then callers authenticate with the admin token (bearer or `X-Admin-Token`, acts as admin) or a `raahi_session` cookie from password or SSO sign-in. Roles: `viewer` may GET, `editor` may mutate gateway configuration, `admin` may also manage users, SSO, the admin token, listener settings, and imports. Insufficient role returns 403. license: name: GPL-3.0-only url: https://www.gnu.org/licenses/gpl-3.0.html servers: - url: / description: The Raahi admin listener serving this contract tags: - name: Applications description: Create an application and its gateway resources together. - name: System description: Open liveness, discovery, and Prometheus endpoints. - name: Admin authentication description: Auth status, admin token, password/SSO sign-in, and the signed-in user. - name: Users description: Admin-UI users and their roles (admin role required). - name: Services description: Logical upstreams and their shared transport settings. - name: Targets description: Concrete upstream hosts belonging to services. - name: Service discovery description: Providers and sources that keep a service's targets in step with an external registry. - name: Routes description: HTTP request matching and service selection. - name: Stream routes description: Raw TCP listeners proxied to services. - name: Plugins description: Traffic policies scoped globally, to services, or to routes. - name: Consumers description: Authenticated identities and ACL groups. - name: Credentials description: Consumer API-key, Basic-auth, and JWT credentials. - name: Certificates description: TLS certificate chains and SNI selection. - name: Settings description: Singleton listener and load-balancing settings. - name: WASM modules description: Uploaded, validated user plugin modules. - name: Observability description: Traffic metrics, request events, health, and route diagnostics. - name: Configuration description: Full configuration backup and restore. security: - AdminBearer: [] - AdminTokenHeader: [] - SessionCookie: [] paths: /api/v1/acme/eab: get: tags: [Certificates] operationId: getAcmeAccountRegistrationStatus summary: Get EAB and account registration status description: Returns only booleans. Credentials are never returned by this endpoint. parameters: - {name: directory_url, in: query, required: true, schema: {type: string}, description: ACME directory URL or provider alias.} responses: '200': description: Registration status for this directory. content: {application/json: {schema: {$ref: '#/components/schemas/AcmeAccountStatus'}}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} put: tags: [Certificates] operationId: setAcmeEabCredentials summary: Save external account binding credentials description: Requires admin. Credentials are scoped to the normalized ACME directory and used only to register a new account. Existing accounts are reused. requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/AcmeEabCredentials'}}} responses: '200': description: Credentials saved. content: {application/json: {schema: {type: object, properties: {configured: {type: boolean}}}}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} delete: tags: [Certificates] operationId: deleteAcmeEabCredentials summary: Remove saved EAB credentials description: Requires admin. Does not delete an existing registered ACME account or prevent its renewal. parameters: - {name: directory_url, in: query, required: true, schema: {type: string}, description: ACME directory URL or provider alias.} responses: '200': description: Credentials removed. content: {application/json: {schema: {type: object, properties: {configured: {type: boolean}}}}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} /api/v1/applications: post: tags: [Applications] operationId: createApplication summary: Add an application atomically description: >- Requires editor or admin. Creates a service, one target, a domain route, and optional HTTPS redirect, HSTS and IP restriction plugins in one transaction. Exact domain conflicts are rejected. HTTPS requires an enabled listener and an issued matching certificate. Existing global policies still apply. DNS is not changed. The completed configuration is hot-reloaded once. requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/ApplicationSpec'}}} responses: '200': description: Application created. content: {application/json: {schema: {$ref: '#/components/schemas/ApplicationCreated'}}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/applications/test-upstream: post: tags: [Applications] operationId: testApplicationUpstream summary: Test an upstream origin from Raahi description: >- Requires editor or admin. Sends one GET with a five-second timeout, verified upstream TLS, no environment proxies, and no redirect following. Any HTTP status counts as reachable. Does not write configuration or consume the response body. requestBody: required: true content: application/json: schema: type: object additionalProperties: false required: [upstream_url] properties: upstream_url: {type: string, example: 'http://127.0.0.1:3000'} responses: '200': description: Connectivity result, including failures. content: {application/json: {schema: {$ref: '#/components/schemas/UpstreamProbe'}}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '500': {$ref: '#/components/responses/InternalError'} /healthz: get: tags: [System] summary: Check whether the admin HTTP server is running operationId: getHealthz security: [] responses: '200': description: The literal string `ok`. content: text/plain: schema: {type: string, enum: [ok]} /metrics: get: tags: [System] summary: Get Prometheus metrics operationId: getPrometheusMetrics security: [] responses: '200': description: Prometheus text exposition. content: text/plain: schema: {type: string} /api/v1/admin/status: get: tags: [Admin authentication] summary: Check whether admin authentication is enabled operationId: getAdminStatus security: [] responses: '200': description: Authentication status. content: application/json: schema: {$ref: '#/components/schemas/AdminStatus'} /api/v1/admin/token: post: tags: [Admin authentication] summary: Generate or rotate the admin token description: The plaintext token is returned exactly once. If auth is already enabled, authenticate this request with the current token. operationId: createAdminToken responses: '200': description: Newly generated token. content: application/json: schema: {$ref: '#/components/schemas/AdminToken'} '401': {$ref: '#/components/responses/Unauthorized'} '500': {$ref: '#/components/responses/InternalError'} delete: tags: [Admin authentication] summary: Disable the admin token description: Users (if any) keep protecting the API; with no users the API becomes open again. operationId: deleteAdminToken responses: '200': description: Token disabled. content: application/json: schema: {$ref: '#/components/schemas/AdminStatus'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/auth/login: post: tags: [Admin authentication] summary: Sign in with email and password description: Sets the `raahi_session` cookie (HttpOnly, SameSite=Lax, 7 days; Secure when reached over HTTPS). Failed attempts are rate limited per client IP. operationId: login security: [] requestBody: {$ref: '#/components/requestBodies/LoginInput'} responses: '200': description: Signed in; the response carries a `Set-Cookie` header. content: {application/json: {schema: {$ref: '#/components/schemas/Principal'}}} '401': {$ref: '#/components/responses/Unauthorized'} '429': description: Too many failed attempts from this client. content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}} '500': {$ref: '#/components/responses/InternalError'} /api/v1/auth/logout: post: tags: [Admin authentication] summary: End the current session operationId: logout responses: '200': description: Session deleted and cookie cleared. content: {application/json: {schema: {type: object, properties: {signed_out: {type: boolean}}}}} '401': {$ref: '#/components/responses/Unauthorized'} /api/v1/auth/me: get: tags: [Admin authentication] summary: Get the caller's identity and effective role operationId: getMe responses: '200': description: Principal. content: {application/json: {schema: {$ref: '#/components/schemas/Principal'}}} '401': {$ref: '#/components/responses/Unauthorized'} /api/v1/auth/me/password: put: tags: [Admin authentication] summary: Change the signed-in user's password description: Requires the current password when one is set. SSO-only accounts may set a first password. operationId: changeOwnPassword requestBody: required: true content: application/json: schema: type: object required: [new_password] properties: current_password: {type: string} new_password: {type: string, minLength: 8} responses: '200': description: Password updated. content: {application/json: {schema: {type: object, properties: {updated: {type: boolean}}}}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} /api/v1/auth/sso/start: get: tags: [Admin authentication] summary: Begin OpenID Connect sign-in description: Browser navigation target. Redirects to the provider's authorization endpoint (authorization code + PKCE). operationId: ssoStart security: [] responses: '303': {description: Redirect to the identity provider.} '400': {$ref: '#/components/responses/BadRequest'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/auth/sso/callback: get: tags: [Admin authentication] summary: OpenID Connect redirect URI description: Exchanges the code, verifies the id_token, signs the user in, and redirects to `/`. Failures redirect to `/?sso_error=...`. operationId: ssoCallback security: [] parameters: - {name: code, in: query, schema: {type: string}} - {name: state, in: query, schema: {type: string}} - {name: error, in: query, schema: {type: string}} - {name: error_description, in: query, schema: {type: string}} responses: '303': {description: Redirect to the UI, with the session cookie set on success.} /api/v1/users: get: tags: [Users] summary: List users operationId: listUsers responses: '200': description: Users ordered by ID. content: {application/json: {schema: {type: array, items: {$ref: '#/components/schemas/User'}}}} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '500': {$ref: '#/components/responses/InternalError'} post: tags: [Users] summary: Create a user description: A password is required unless SSO is configured (SSO-only account). operationId: createUser requestBody: {$ref: '#/components/requestBodies/UserInput'} responses: '200': description: Created user. content: {application/json: {schema: {$ref: '#/components/schemas/User'}}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/users/{id}: parameters: - $ref: '#/components/parameters/Id' put: tags: [Users] summary: Update a user description: Omit `password` to keep the current one; supplying one signs that user out everywhere. The last admin cannot be demoted. operationId: updateUser requestBody: {$ref: '#/components/requestBodies/UserInput'} responses: '200': description: Updated user. content: {application/json: {schema: {$ref: '#/components/schemas/User'}}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} '500': {$ref: '#/components/responses/InternalError'} delete: tags: [Users] summary: Delete a user description: Their sessions end immediately. The last admin cannot be deleted. operationId: deleteUser responses: '200': {$ref: '#/components/responses/Deleted'} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/sso/config: get: tags: [Admin authentication] summary: Get SSO settings (secret redacted) operationId: getSsoConfig responses: '200': description: SSO settings. content: {application/json: {schema: {$ref: '#/components/schemas/SsoStatus'}}} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} put: tags: [Admin authentication] summary: Configure OpenID Connect SSO description: Omit `client_secret` to keep the stored one. The issuer must use HTTPS (HTTP is allowed for localhost only). operationId: setSsoConfig requestBody: {$ref: '#/components/requestBodies/SsoConfigInput'} responses: '200': description: Saved settings. content: {application/json: {schema: {$ref: '#/components/schemas/SsoStatus'}}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '500': {$ref: '#/components/responses/InternalError'} delete: tags: [Admin authentication] summary: Disable SSO operationId: deleteSsoConfig responses: '200': description: SSO disabled. content: {application/json: {schema: {$ref: '#/components/schemas/SsoStatus'}}} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/services: get: tags: [Services] summary: List services operationId: listServices responses: '200': description: Services ordered by ID. content: application/json: schema: {type: array, items: {$ref: '#/components/schemas/Service'}} '401': {$ref: '#/components/responses/Unauthorized'} '500': {$ref: '#/components/responses/InternalError'} post: tags: [Services] summary: Create a service description: Proxy services require Editor. Static services or writes carrying static configuration require Admin. operationId: createService requestBody: {$ref: '#/components/requestBodies/ServiceInput'} responses: '200': description: Created service. content: application/json: schema: {$ref: '#/components/schemas/Service'} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/services/{id}: parameters: - $ref: '#/components/parameters/Id' get: tags: [Services] summary: Get a service operationId: getService responses: '200': description: Service. content: {application/json: {schema: {$ref: '#/components/schemas/Service'}}} '401': {$ref: '#/components/responses/Unauthorized'} '404': {$ref: '#/components/responses/NotFound'} '500': {$ref: '#/components/responses/InternalError'} put: tags: [Services] summary: Replace a service description: Full replacement; omitted optional fields reset to their defaults. Writes involving an existing or submitted static service, including conversions, require Admin. Other service writes require Editor. operationId: updateService requestBody: {$ref: '#/components/requestBodies/ServiceInput'} responses: '200': description: Updated service. content: {application/json: {schema: {$ref: '#/components/schemas/Service'}}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} '500': {$ref: '#/components/responses/InternalError'} delete: tags: [Services] summary: Delete a service description: Also removes dependent targets, routes, stream routes, and plugins. Deleting a static service requires Admin; deleting a proxy service requires Editor. operationId: deleteService responses: '200': {$ref: '#/components/responses/Deleted'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/services/{service_id}/targets: parameters: - $ref: '#/components/parameters/ServiceId' get: tags: [Targets] summary: List a service's targets description: Returns an empty array when the parent service does not exist. operationId: listServiceTargets responses: '200': description: Targets ordered by ID. content: application/json: schema: {type: array, items: {$ref: '#/components/schemas/Target'}} '401': {$ref: '#/components/responses/Unauthorized'} '500': {$ref: '#/components/responses/InternalError'} post: tags: [Targets] summary: Add a target to a service operationId: createTarget requestBody: {$ref: '#/components/requestBodies/TargetInput'} responses: '200': description: Created target. content: {application/json: {schema: {$ref: '#/components/schemas/Target'}}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/targets/{id}: parameters: - $ref: '#/components/parameters/Id' put: tags: [Targets] summary: Replace a target operationId: updateTarget requestBody: {$ref: '#/components/requestBodies/TargetInput'} responses: '200': description: Updated target. content: {application/json: {schema: {$ref: '#/components/schemas/Target'}}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '404': {$ref: '#/components/responses/NotFound'} '500': {$ref: '#/components/responses/InternalError'} delete: tags: [Targets] summary: Delete a target operationId: deleteTarget responses: '200': {$ref: '#/components/responses/Deleted'} '401': {$ref: '#/components/responses/Unauthorized'} '404': {$ref: '#/components/responses/NotFound'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/discovery/providers: get: tags: [Service discovery] summary: List discovery providers description: >- Static metadata for every built-in provider: its configuration keys, defaults, and which endpoint fields it supplies itself. Nothing here depends on stored configuration. operationId: listDiscoveryProviders responses: '200': description: Providers ordered by ID. content: application/json: schema: {type: array, items: {$ref: '#/components/schemas/DiscoveryProvider'}} '401': {$ref: '#/components/responses/Unauthorized'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/services/{service_id}/discovery-sources: parameters: - $ref: '#/components/parameters/ServiceId' get: tags: [Service discovery] summary: List a service's discovery sources description: Returns an empty array when the parent service does not exist. operationId: listServiceDiscoverySources responses: '200': description: Discovery sources ordered by ID. content: application/json: schema: {type: array, items: {$ref: '#/components/schemas/DiscoverySource'}} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '500': {$ref: '#/components/responses/InternalError'} post: tags: [Service discovery] summary: Add a discovery source to a service description: >- The config is validated against the selected provider and the first refresh runs in the background. Targets created by a source are owned by it and are not editable through the target endpoints. operationId: createDiscoverySource requestBody: {$ref: '#/components/requestBodies/DiscoverySourceInput'} responses: '200': description: Created discovery source. content: {application/json: {schema: {$ref: '#/components/schemas/DiscoverySource'}}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/discovery-sources/{id}: parameters: - $ref: '#/components/parameters/Id' get: tags: [Service discovery] summary: Get a discovery source operationId: getDiscoverySource responses: '200': description: Discovery source. content: {application/json: {schema: {$ref: '#/components/schemas/DiscoverySource'}}} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} '500': {$ref: '#/components/responses/InternalError'} put: tags: [Service discovery] summary: Replace a discovery source description: >- Full replacement; omitted optional fields reset to their defaults. Changing the provider or its config discards the current endpoint set and resolves again from scratch; targets that the new config no longer returns leave through the removal grace period. operationId: updateDiscoverySource requestBody: {$ref: '#/components/requestBodies/DiscoverySourceInput'} responses: '200': description: Updated discovery source. content: {application/json: {schema: {$ref: '#/components/schemas/DiscoverySource'}}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} '500': {$ref: '#/components/responses/InternalError'} delete: tags: [Service discovery] summary: Delete a discovery source description: Also deletes every target the source owns, immediately and without the removal grace period. operationId: deleteDiscoverySource responses: '200': {$ref: '#/components/responses/Deleted'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/discovery-sources/{id}/refresh: parameters: - $ref: '#/components/parameters/Id' post: tags: [Service discovery] summary: Refresh a discovery source now description: >- Queues an immediate background refresh instead of waiting for the next interval and returns the source's current status. Poll the status endpoint to observe completion or failure. operationId: refreshDiscoverySource responses: '200': description: Current status after the refresh was queued. content: {application/json: {schema: {$ref: '#/components/schemas/DiscoverySourceStatus'}}} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/discovery-sources/{id}/status: parameters: - $ref: '#/components/parameters/Id' get: tags: [Service discovery] summary: Get discovery source status description: Runtime state of the source, covering its last attempt and success, endpoint revision, and target counts by state. operationId: getDiscoverySourceStatus responses: '200': description: Discovery source status. content: {application/json: {schema: {$ref: '#/components/schemas/DiscoverySourceStatus'}}} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/routes: get: tags: [Routes] summary: List HTTP routes operationId: listRoutes responses: '200': description: Routes ordered by ID. content: application/json: schema: {type: array, items: {$ref: '#/components/schemas/Route'}} '401': {$ref: '#/components/responses/Unauthorized'} '500': {$ref: '#/components/responses/InternalError'} post: tags: [Routes] summary: Create an HTTP route operationId: createRoute requestBody: {$ref: '#/components/requestBodies/RouteInput'} responses: '200': description: Created route. content: {application/json: {schema: {$ref: '#/components/schemas/Route'}}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/routes/{id}: parameters: - $ref: '#/components/parameters/Id' get: tags: [Routes] summary: Get an HTTP route operationId: getRoute responses: '200': description: Route. content: {application/json: {schema: {$ref: '#/components/schemas/Route'}}} '401': {$ref: '#/components/responses/Unauthorized'} '404': {$ref: '#/components/responses/NotFound'} '500': {$ref: '#/components/responses/InternalError'} put: tags: [Routes] summary: Replace an HTTP route operationId: updateRoute requestBody: {$ref: '#/components/requestBodies/RouteInput'} responses: '200': description: Updated route. content: {application/json: {schema: {$ref: '#/components/schemas/Route'}}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '404': {$ref: '#/components/responses/NotFound'} '500': {$ref: '#/components/responses/InternalError'} delete: tags: [Routes] summary: Delete an HTTP route operationId: deleteRoute responses: '200': {$ref: '#/components/responses/Deleted'} '401': {$ref: '#/components/responses/Unauthorized'} '404': {$ref: '#/components/responses/NotFound'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/stream-routes: get: tags: [Stream routes] summary: List L4 stream routes operationId: listStreamRoutes responses: '200': description: Stream routes ordered by ID. content: application/json: schema: {type: array, items: {$ref: '#/components/schemas/StreamRoute'}} '401': {$ref: '#/components/responses/Unauthorized'} '500': {$ref: '#/components/responses/InternalError'} post: tags: [Stream routes] summary: Create an L4 stream route description: The listener is persisted immediately but must be bound by restarting Raahi. operationId: createStreamRoute requestBody: {$ref: '#/components/requestBodies/StreamRouteInput'} responses: '200': description: Created route with a restart note. content: {application/json: {schema: {$ref: '#/components/schemas/StreamRouteResult'}}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/stream-routes/{id}: parameters: - $ref: '#/components/parameters/Id' put: tags: [Stream routes] summary: Replace an L4 stream route description: Retargeting applies live. Changing `listen_addr` requires a restart and adds `note` to the response. operationId: updateStreamRoute requestBody: {$ref: '#/components/requestBodies/StreamRouteInput'} responses: '200': description: Updated stream route, optionally with a restart note. content: {application/json: {schema: {$ref: '#/components/schemas/StreamRouteResult'}}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '404': {$ref: '#/components/responses/NotFound'} '500': {$ref: '#/components/responses/InternalError'} delete: tags: [Stream routes] summary: Delete an L4 stream route description: Restart Raahi to unbind the listener. operationId: deleteStreamRoute responses: '200': description: Deletion result with a restart note. content: {application/json: {schema: {$ref: '#/components/schemas/StreamRouteDeleteResult'}}} '401': {$ref: '#/components/responses/Unauthorized'} '404': {$ref: '#/components/responses/NotFound'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/plugins: get: tags: [Plugins] summary: List plugins operationId: listPlugins responses: '200': description: Plugins ordered by ordering then ID. content: application/json: schema: {type: array, items: {$ref: '#/components/schemas/Plugin'}} '401': {$ref: '#/components/responses/Unauthorized'} '500': {$ref: '#/components/responses/InternalError'} post: tags: [Plugins] summary: Create a plugin operationId: createPlugin requestBody: {$ref: '#/components/requestBodies/PluginInput'} responses: '200': description: Created plugin. content: {application/json: {schema: {$ref: '#/components/schemas/Plugin'}}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/plugins/{id}: parameters: - $ref: '#/components/parameters/Id' get: tags: [Plugins] summary: Get a plugin operationId: getPlugin responses: '200': description: Plugin. content: {application/json: {schema: {$ref: '#/components/schemas/Plugin'}}} '401': {$ref: '#/components/responses/Unauthorized'} '404': {$ref: '#/components/responses/NotFound'} '500': {$ref: '#/components/responses/InternalError'} put: tags: [Plugins] summary: Replace a plugin operationId: updatePlugin requestBody: {$ref: '#/components/requestBodies/PluginInput'} responses: '200': description: Updated plugin. content: {application/json: {schema: {$ref: '#/components/schemas/Plugin'}}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '404': {$ref: '#/components/responses/NotFound'} '500': {$ref: '#/components/responses/InternalError'} delete: tags: [Plugins] summary: Delete a plugin operationId: deletePlugin responses: '200': {$ref: '#/components/responses/Deleted'} '401': {$ref: '#/components/responses/Unauthorized'} '404': {$ref: '#/components/responses/NotFound'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/cache/purge: post: tags: [Plugins] summary: Purge all in-memory proxy-cache entries operationId: purgeProxyCache responses: '200': description: Number of entries removed. content: application/json: schema: {$ref: '#/components/schemas/PurgeResult'} '401': {$ref: '#/components/responses/Unauthorized'} /api/v1/consumers: get: tags: [Consumers] summary: List consumers operationId: listConsumers responses: '200': description: Consumers ordered by ID. content: application/json: schema: {type: array, items: {$ref: '#/components/schemas/Consumer'}} '401': {$ref: '#/components/responses/Unauthorized'} '500': {$ref: '#/components/responses/InternalError'} post: tags: [Consumers] summary: Create a consumer operationId: createConsumer requestBody: {$ref: '#/components/requestBodies/ConsumerInput'} responses: '200': description: Created consumer. content: {application/json: {schema: {$ref: '#/components/schemas/Consumer'}}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/consumers/{id}: parameters: - $ref: '#/components/parameters/Id' get: tags: [Consumers] summary: Get a consumer operationId: getConsumer responses: '200': description: Consumer. content: {application/json: {schema: {$ref: '#/components/schemas/Consumer'}}} '401': {$ref: '#/components/responses/Unauthorized'} '404': {$ref: '#/components/responses/NotFound'} '500': {$ref: '#/components/responses/InternalError'} put: tags: [Consumers] summary: Replace a consumer operationId: updateConsumer requestBody: {$ref: '#/components/requestBodies/ConsumerInput'} responses: '200': description: Updated consumer. content: {application/json: {schema: {$ref: '#/components/schemas/Consumer'}}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '404': {$ref: '#/components/responses/NotFound'} '500': {$ref: '#/components/responses/InternalError'} delete: tags: [Consumers] summary: Delete a consumer and its credentials operationId: deleteConsumer responses: '200': {$ref: '#/components/responses/Deleted'} '401': {$ref: '#/components/responses/Unauthorized'} '404': {$ref: '#/components/responses/NotFound'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/consumers/{consumer_id}/credentials: parameters: - $ref: '#/components/parameters/ConsumerId' get: tags: [Credentials] summary: List a consumer's credentials description: Secrets are never included. A missing parent returns an empty array. operationId: listConsumerCredentials responses: '200': description: Redacted credentials. content: application/json: schema: {type: array, items: {$ref: '#/components/schemas/Credential'}} '401': {$ref: '#/components/responses/Unauthorized'} '500': {$ref: '#/components/responses/InternalError'} post: tags: [Credentials] summary: Create a credential description: Basic-auth and JWT secrets are accepted but never returned. operationId: createCredential requestBody: {$ref: '#/components/requestBodies/CredentialInput'} responses: '200': description: Created credential with its secret redacted. content: {application/json: {schema: {$ref: '#/components/schemas/Credential'}}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/credentials/{id}: parameters: - $ref: '#/components/parameters/Id' delete: tags: [Credentials] summary: Delete a credential operationId: deleteCredential responses: '200': {$ref: '#/components/responses/Deleted'} '401': {$ref: '#/components/responses/Unauthorized'} '404': {$ref: '#/components/responses/NotFound'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/certificates: get: tags: [Certificates] summary: List TLS certificates description: Private keys are never returned. operationId: listCertificates responses: '200': description: Certificates with private keys redacted. content: application/json: schema: {type: array, items: {$ref: '#/components/schemas/Certificate'}} '401': {$ref: '#/components/responses/Unauthorized'} '500': {$ref: '#/components/responses/InternalError'} post: tags: [Certificates] summary: Add a TLS certificate operationId: createCertificate requestBody: {$ref: '#/components/requestBodies/CertificateInput'} responses: '200': description: Created certificate without its private key. content: {application/json: {schema: {$ref: '#/components/schemas/Certificate'}}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/certificates/{id}: parameters: - $ref: '#/components/parameters/Id' delete: tags: [Certificates] summary: Delete a TLS certificate operationId: deleteCertificate responses: '200': {$ref: '#/components/responses/Deleted'} '401': {$ref: '#/components/responses/Unauthorized'} '404': {$ref: '#/components/responses/NotFound'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/certificates/{id}/renew: parameters: - $ref: '#/components/parameters/Id' post: tags: [Certificates] summary: Queue renewal of an ACME-managed certificate operationId: renewCertificate responses: '200': description: Renewal queued. content: {application/json: {schema: {type: object, properties: {queued: {type: boolean}}}}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '404': {$ref: '#/components/responses/NotFound'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/acme/cloudflare-token: get: tags: [Certificates] summary: Check whether a Cloudflare API token is configured operationId: getCloudflareTokenStatus responses: '200': description: Token status; the token itself is never returned. content: {application/json: {schema: {type: object, properties: {configured: {type: boolean}}}}} '401': {$ref: '#/components/responses/Unauthorized'} '500': {$ref: '#/components/responses/InternalError'} put: tags: [Certificates] summary: Store the Cloudflare API token used for DNS-01 operationId: setCloudflareToken requestBody: required: true content: application/json: schema: type: object required: [token] properties: {token: {type: string, minLength: 1, writeOnly: true}} responses: '200': {description: Token stored.} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '500': {$ref: '#/components/responses/InternalError'} delete: tags: [Certificates] summary: Remove the Cloudflare API token operationId: deleteCloudflareToken responses: '200': {description: Token removed.} '401': {$ref: '#/components/responses/Unauthorized'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/settings: get: tags: [Settings] summary: Get global settings operationId: getSettings responses: '200': description: Current settings. content: {application/json: {schema: {$ref: '#/components/schemas/Settings'}}} '401': {$ref: '#/components/responses/Unauthorized'} '500': {$ref: '#/components/responses/InternalError'} put: tags: [Settings] summary: Replace global settings description: Listener-address changes may require a restart because listeners bind at startup. operationId: updateSettings requestBody: {$ref: '#/components/requestBodies/SettingsInput'} responses: '200': description: Updated settings. content: {application/json: {schema: {$ref: '#/components/schemas/Settings'}}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/wasm-modules: get: tags: [WASM modules] summary: List uploaded WASM modules description: Module bytes are never returned. operationId: listWasmModules responses: '200': description: WASM module metadata. content: application/json: schema: {type: array, items: {$ref: '#/components/schemas/WasmModule'}} '401': {$ref: '#/components/responses/Unauthorized'} '500': {$ref: '#/components/responses/InternalError'} post: tags: [WASM modules] summary: Upload and validate a WASM module description: Supply a non-empty `wasm_base64` or `wat`. When both are present, `wasm_base64` takes precedence. operationId: createWasmModule requestBody: {$ref: '#/components/requestBodies/WasmModuleInput'} responses: '200': description: Created module metadata. content: {application/json: {schema: {$ref: '#/components/schemas/WasmModule'}}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/wasm-modules/{id}: parameters: - $ref: '#/components/parameters/Id' delete: tags: [WASM modules] summary: Delete a WASM module description: Fails while a WASM plugin references the module by name. operationId: deleteWasmModule responses: '200': {$ref: '#/components/responses/Deleted'} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '404': {$ref: '#/components/responses/NotFound'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/metrics: get: tags: [Observability] summary: Get JSON traffic metrics operationId: getMetrics responses: '200': description: In-memory metrics snapshot. content: {application/json: {schema: {$ref: '#/components/schemas/MetricsSnapshot'}}} '401': {$ref: '#/components/responses/Unauthorized'} /api/v1/requests: get: tags: [Observability] summary: List recent completed requests description: Newest first from an in-memory ring buffer containing at most 500 records. operationId: listRecentRequests parameters: - name: limit in: query description: Requested record count. Values over 500 are clamped to 500. schema: {type: integer, minimum: 0, maximum: 500, default: 100} responses: '200': description: Recent requests. content: application/json: schema: {type: array, items: {$ref: '#/components/schemas/RequestRecord'}} '401': {$ref: '#/components/responses/Unauthorized'} /api/v1/events: get: tags: [Observability] summary: Stream completed requests with server-sent events description: Each data event is a JSON RequestRecord. Query-token auth is supported for browser EventSource, but URLs may be logged. operationId: streamRequestEvents security: - AdminBearer: [] - AdminTokenHeader: [] - EventAccessToken: [] responses: '200': description: Never-ending SSE stream with keep-alives. Lagging clients may lose events. content: text/event-stream: schema: {type: string} '401': {$ref: '#/components/responses/Unauthorized'} /api/v1/config: get: tags: [Observability] summary: Get live compiled-config counts and version operationId: getConfigSummary responses: '200': description: Snapshot summary. content: {application/json: {schema: {$ref: '#/components/schemas/ConfigSummary'}}} '401': {$ref: '#/components/responses/Unauthorized'} /api/v1/health: get: tags: [Observability] summary: Get live upstream target health operationId: listTargetHealth responses: '200': description: Health for each compiled target. content: application/json: schema: {type: array, items: {$ref: '#/components/schemas/TargetHealth'}} '401': {$ref: '#/components/responses/Unauthorized'} /api/v1/router/test: get: tags: [Observability] summary: Dry-run HTTP route matching operationId: testRouter parameters: - name: path in: query required: true schema: {type: string, example: /orders/42} - name: host in: query schema: {type: string, default: '', example: api.example.com} - name: method in: query schema: {type: string, default: GET, example: GET} - name: headers in: query description: Comma-separated `Name:value` pairs. schema: {type: string, example: 'X-Region:in,X-Version:v2'} responses: '200': description: Match result. content: {application/json: {schema: {$ref: '#/components/schemas/RouterTestResult'}}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} /api/v1/export: get: tags: [Configuration] summary: Export the complete configuration description: Setting `include_secrets=true` creates a sensitive restorable backup containing private keys and stored credential material. operationId: exportConfiguration parameters: - name: include_secrets in: query schema: {type: boolean, default: false} responses: '200': description: Export document. Secret fields may be omitted depending on the query. content: {application/json: {schema: {$ref: '#/components/schemas/ExportDocument'}}} '401': {$ref: '#/components/responses/Unauthorized'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/import: post: tags: [Configuration] summary: Replace configuration from an export document description: Destructive transactional full replacement with ID remapping. Take an export first. The admin token is not replaced. operationId: importConfiguration requestBody: {$ref: '#/components/requestBodies/ImportDocument'} responses: '200': description: Counts restored plus skipped items and reasons. content: {application/json: {schema: {$ref: '#/components/schemas/ImportReport'}}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/config/apply: post: tags: [Configuration] summary: Apply a declarative config file description: >- Changes the configuration to match a config file, in one transaction. The Content-Type selects JSON, YAML, or HUML. Raahi matches entities by name, so unchanged ones keep their IDs, discovered targets, health state, and issued certificates. Raahi leaves a top-level section alone if the file doesn't have it, and deletes unlisted entries from a section the file does have, even an empty one. An omitted credential secret or certificate PEM keeps the stored value. `raahi apply` calls this endpoint after filling `${VAR}` references from the client environment. The server itself treats `${VAR}` as literal text. Admin only. operationId: applyConfigFile parameters: - name: dry_run in: query description: Report the planned changes without applying them. schema: {type: boolean, default: false} requestBody: required: true content: application/json: {schema: {$ref: '#/components/schemas/ConfigDocument'}} application/yaml: {schema: {$ref: '#/components/schemas/ConfigDocument'}} application/huml: {schema: {$ref: '#/components/schemas/ConfigDocument'}} responses: '200': description: The changes applied, or planned for a dry run. content: {application/json: {schema: {$ref: '#/components/schemas/ApplyReport'}}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '500': {$ref: '#/components/responses/InternalError'} /api/v1/config/current: get: tags: [Configuration] summary: Dump the current configuration as a config file description: >- The dump has every section. It leaves out credential secrets and certificate PEMs but contains key-auth identifiers, which are the API keys themselves, plus plugin and discovery configs. Applying the dump unchanged changes nothing. Admin only. operationId: currentConfigFile parameters: - name: format in: query schema: {type: string, enum: [yaml, huml, json], default: yaml} responses: '200': description: Config file in the requested format. content: application/yaml: {schema: {$ref: '#/components/schemas/ConfigDocument'}} application/huml: {schema: {$ref: '#/components/schemas/ConfigDocument'}} application/json: {schema: {$ref: '#/components/schemas/ConfigDocument'}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '500': {$ref: '#/components/responses/InternalError'} components: securitySchemes: AdminBearer: type: http scheme: bearer bearerFormat: Raahi admin token description: Optional until a token is generated. AdminTokenHeader: type: apiKey in: header name: X-Admin-Token description: Alternative to the Authorization bearer header. EventAccessToken: type: apiKey in: query name: access_token description: SSE-only fallback for browser EventSource clients. Prefer headers elsewhere. SessionCookie: type: apiKey in: cookie name: raahi_session description: Issued by password or SSO sign-in. Carries the user's role. parameters: Id: name: id in: path required: true schema: {$ref: '#/components/schemas/Id'} ServiceId: name: service_id in: path required: true schema: {$ref: '#/components/schemas/Id'} ConsumerId: name: consumer_id in: path required: true schema: {$ref: '#/components/schemas/Id'} requestBodies: ServiceInput: required: true content: {application/json: {schema: {$ref: '#/components/schemas/ServiceInput'}}} TargetInput: required: true content: {application/json: {schema: {$ref: '#/components/schemas/TargetInput'}}} DiscoverySourceInput: required: true content: {application/json: {schema: {$ref: '#/components/schemas/DiscoverySourceInput'}}} RouteInput: required: true content: {application/json: {schema: {$ref: '#/components/schemas/RouteInput'}}} StreamRouteInput: required: true content: {application/json: {schema: {$ref: '#/components/schemas/StreamRouteInput'}}} PluginInput: required: true content: {application/json: {schema: {$ref: '#/components/schemas/PluginInput'}}} ConsumerInput: required: true content: {application/json: {schema: {$ref: '#/components/schemas/ConsumerInput'}}} CredentialInput: required: true content: {application/json: {schema: {$ref: '#/components/schemas/CredentialInput'}}} CertificateInput: required: true content: {application/json: {schema: {$ref: '#/components/schemas/CertificateInput'}}} LoginInput: required: true content: application/json: schema: type: object required: [email, password] properties: email: {type: string, format: email} password: {type: string} UserInput: required: true content: {application/json: {schema: {$ref: '#/components/schemas/UserInput'}}} SsoConfigInput: required: true content: {application/json: {schema: {$ref: '#/components/schemas/SsoConfigInput'}}} SettingsInput: required: true content: {application/json: {schema: {$ref: '#/components/schemas/SettingsInput'}}} WasmModuleInput: required: true content: {application/json: {schema: {$ref: '#/components/schemas/WasmModuleInput'}}} ImportDocument: required: true content: {application/json: {schema: {$ref: '#/components/schemas/ImportDocument'}}} responses: Deleted: description: Resource deleted. content: {application/json: {schema: {$ref: '#/components/schemas/DeleteResult'}}} BadRequest: description: Validation, conflict, foreign-key, or malformed-input error. content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}} Forbidden: description: Authenticated, but the role is insufficient. content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}} Unauthorized: description: Admin auth is enabled and the token is missing or invalid. content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}} NotFound: description: Resource not found. content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}} InternalError: description: Internal storage or server error. content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}} schemas: ApplicationSpec: type: object additionalProperties: false required: [name, domain, upstream_url] properties: name: {type: string, minLength: 1, maxLength: 100, example: Photos} domain: {type: string, example: photos.example.com, description: Exact DNS hostname, without scheme, path, port or wildcard.} upstream_url: {type: string, example: 'http://127.0.0.1:3000', description: HTTP or HTTPS origin without credentials, a path other than slash, query or fragment. IPv6 is supported.} https: {type: boolean, default: false, description: Redirect HTTP to HTTPS with status 308. Requires an existing matching certificate and HTTPS listener.} hsts: {type: boolean, default: false, description: Enable HSTS for one year without subdomains or preload. Requires https.} allowed_cidrs: {type: array, maxItems: 100, items: {type: string}, default: [], description: Allow only these client IPs or CIDRs. Empty means no additional IP restriction.} ApplicationCreated: type: object required: [name, url, service_id, target_id, route_id, plugin_ids] properties: name: {type: string} url: {type: string} service_id: {$ref: '#/components/schemas/Id'} target_id: {$ref: '#/components/schemas/Id'} route_id: {$ref: '#/components/schemas/Id'} plugin_ids: {type: array, items: {$ref: '#/components/schemas/Id'}} UpstreamProbe: type: object required: [reachable, status, latency_ms, message] properties: reachable: {type: boolean} status: {type: integer, nullable: true} latency_ms: {type: integer, minimum: 0} message: {type: string} Id: type: integer format: int64 minimum: 1 example: 42 Timestamp: type: string format: date-time example: '2026-08-16T09:30:00Z' Protocol: type: string enum: [http, https] LoadBalancingAlgorithm: type: string enum: [round_robin, random, consistent, weighted] PluginType: type: string enum: [key-auth, basic-auth, jwt, acl, ip-restriction, rate-limit, proxy-cache, request-size-limit, request-termination, redirect, cors, wasm, request-transform, response-transform, hsts, response-body-transform, http-log, request-id, response-compression] PluginScope: type: string enum: [global, service, route] CredentialType: type: string enum: [key-auth, basic-auth, jwt] Error: type: object required: [error] properties: error: {type: string, example: not found} DeleteResult: type: object required: [deleted] properties: deleted: {type: boolean, enum: [true]} AdminStatus: type: object required: [auth_enabled] properties: auth_enabled: {type: boolean, description: True when an admin token or any user exists.} token_enabled: {type: boolean} users_exist: {type: boolean} sso: type: object properties: enabled: {type: boolean} label: {type: string, example: Google} Role: type: string enum: [viewer, editor, admin] User: type: object required: [id, email, name, role, has_password, created_at, last_login_at] properties: id: {$ref: '#/components/schemas/Id'} email: {type: string, format: email} name: {type: string} role: {$ref: '#/components/schemas/Role'} has_password: {type: boolean, description: False for SSO-only accounts.} created_at: {$ref: '#/components/schemas/Timestamp'} last_login_at: {allOf: [{$ref: '#/components/schemas/Timestamp'}], nullable: true} UserInput: type: object required: [email, role] properties: email: {type: string, format: email} name: {type: string, default: ''} role: {$ref: '#/components/schemas/Role'} password: {type: string, minLength: 8, writeOnly: true, description: Omit on update to keep the current password.} Principal: type: object required: [role, method, user] properties: role: {$ref: '#/components/schemas/Role'} method: {type: string, enum: [open, token, session]} user: {allOf: [{$ref: '#/components/schemas/User'}], nullable: true} SsoConfigInput: type: object required: [issuer, client_id] properties: issuer: {type: string, format: uri, example: 'https://accounts.google.com'} client_id: {type: string} client_secret: {type: string, writeOnly: true} label: {type: string, description: Sign-in button label.} auto_provision_role: allOf: [{$ref: '#/components/schemas/Role'}] nullable: true description: Create unknown users on first SSO login with this role; null = only pre-created users may sign in. allowed_domains: type: array items: {type: string} description: Email domains eligible for auto-provisioning. Empty = any. SsoStatus: type: object required: [enabled, config] properties: enabled: {type: boolean} config: type: object nullable: true properties: issuer: {type: string} client_id: {type: string} client_secret_set: {type: boolean} label: {type: string} auto_provision_role: {allOf: [{$ref: '#/components/schemas/Role'}], nullable: true} allowed_domains: {type: array, items: {type: string}} AdminToken: type: object required: [token, note] properties: token: type: string pattern: '^[0-9a-f]{64}$' writeOnly: true description: Returned only by this operation. note: {type: string} ServiceInput: type: object required: [name] properties: name: {type: string, minLength: 1, example: orders} kind: {type: string, enum: [proxy, static], default: proxy, description: "Static services serve files directly on the existing listeners and have no upstream targets."} root: {type: string, nullable: true, example: /srv/www/docs, description: "Absolute directory on the Raahi host. Required for kind static; omitted for proxy. Requires Admin. Cannot be /, lie under /proc, /sys or /dev, or contain Raahi's database directory. Existing symlink components are resolved for these checks."} spa_fallback: {type: boolean, default: false, description: "Static services only. Missing extensionless paths serve the root index.html."} protocol: {$ref: '#/components/schemas/Protocol'} connect_timeout_ms: {type: integer, format: int64, minimum: 0, default: 5000} read_timeout_ms: {type: integer, format: int64, minimum: 0, default: 60000} write_timeout_ms: {type: integer, format: int64, minimum: 0, default: 60000} retries: {type: integer, format: int32, minimum: 0, default: 1} lb_algorithm: {$ref: '#/components/schemas/LoadBalancingAlgorithm'} tls_sni: {type: string, nullable: true, example: upstream.example.com, description: SNI presented to an HTTPS upstream. Unset it falls back to upstream_authority, then the target host.} upstream_authority: {type: string, nullable: true, example: orders.internal, description: "Host and :authority sent upstream when the route does not preserve the incoming Host header, given as host or host:port. Discovered targets are usually bare addresses, so set this when the application routes on the Host header. Null uses the target host."} health_path: {type: string, nullable: true, example: /healthz, description: "HTTP GET health check path; HTTPS services use verified HTTPS with tls_sni. Blank selects TCP checks."} Service: allOf: - type: object required: [id, name, kind, root, spa_fallback, protocol, connect_timeout_ms, read_timeout_ms, write_timeout_ms, retries, lb_algorithm, tls_sni, upstream_authority, health_path, created_at, updated_at] properties: id: {$ref: '#/components/schemas/Id'} created_at: {$ref: '#/components/schemas/Timestamp'} updated_at: {$ref: '#/components/schemas/Timestamp'} - $ref: '#/components/schemas/ServiceInput' TargetInput: type: object required: [host, port] properties: host: {type: string, minLength: 1, example: 127.0.0.1} port: {type: integer, minimum: 0, maximum: 65535, example: 9001} weight: {type: integer, format: int32, minimum: 0, default: 100} priority: {type: integer, format: int32, minimum: 0, maximum: 65535, default: 0, description: Failover tier. Raahi balances across the healthy targets holding the lowest priority value and uses a higher value only when every target below it is unavailable.} enabled: {type: boolean, default: true} TargetState: type: string enum: [active, draining, stale] description: >- active takes new traffic; draining finishes in-flight requests after the endpoint left a successful refresh and is deleted when removal_grace_ms expires; stale is a last known good endpoint retained after refreshes stopped succeeding for longer than stale_after_ms. It remains visible for diagnosis but does not take new traffic. Target: allOf: - type: object required: [id, service_id, host, port, weight, priority, enabled, state, source_id, provider_key, metadata, last_seen_at, missing_since] properties: id: {$ref: '#/components/schemas/Id'} service_id: {$ref: '#/components/schemas/Id'} state: {$ref: '#/components/schemas/TargetState'} source_id: {allOf: [{$ref: '#/components/schemas/NullableId'}], description: Discovery source that owns this target. Null for targets created by an operator.} provider_key: {type: string, nullable: true, example: 'orders-7f9c.internal:8080', description: Stable identity the provider reported for this endpoint. Raahi matches it across refreshes so an endpoint that returns keeps its health state instead of starting over.} metadata: {type: object, additionalProperties: {type: string}, default: {}, description: "Labels the provider returned with the endpoint, such as a zone or version. Empty for operator-created targets."} last_seen_at: {allOf: [{$ref: '#/components/schemas/Timestamp'}], nullable: true, description: Last successful discovery refresh that included this endpoint. Null for manual targets.} missing_since: {allOf: [{$ref: '#/components/schemas/Timestamp'}], nullable: true, description: When an endpoint first disappeared from a successful refresh. Set only while draining.} - $ref: '#/components/schemas/TargetInput' DiscoveryProviderId: type: string enum: [dns, dns-srv, http] DiscoveryProvider: type: object required: [id, name, description, supplies_port, supplies_weight, supplies_priority, config_schema, defaults] properties: id: {$ref: '#/components/schemas/DiscoveryProviderId'} name: {type: string, example: DNS A and AAAA} description: {type: string, example: Resolves a hostname and turns every returned address into a target.} supplies_port: {type: boolean, description: "Whether the provider reports a port. When false, config.port is required and applies to every endpoint."} supplies_weight: {type: boolean, description: "Whether the provider reports a weight. When false, config.weight applies to every endpoint."} supplies_priority: {type: boolean, description: "Whether the provider reports a priority. When false, config.priority applies to every endpoint."} config_schema: {type: object, additionalProperties: true, description: "JSON Schema for this provider's config object, for building a form without hard-coding provider fields."} defaults: {type: object, additionalProperties: true, description: Config values applied when a source omits them.} DiscoveryConfigCommon: type: object description: Scheduling and endpoint defaults shared by every provider. properties: interval_ms: {type: integer, format: int64, minimum: 1000, default: 30000, description: Maximum time between scheduled refreshes. Raahi subtracts up to 10 percent jitter, and DNS sources refresh sooner when the record TTL is shorter.} timeout_ms: {type: integer, format: int64, minimum: 100, default: 5000, description: Deadline for one refresh attempt. A refresh that runs out of time counts as a failure.} port: {type: integer, minimum: 1, maximum: 65535, nullable: true, description: Port applied to every endpoint the provider returns without one. Required when the provider does not supply a port.} weight: {type: integer, format: int32, minimum: 0, default: 100, description: Weight applied to endpoints the provider returns without one.} priority: {type: integer, format: int32, minimum: 0, maximum: 65535, default: 0, description: Priority applied to endpoints the provider returns without one.} DnsDiscoveryConfig: allOf: - type: object required: [hostname, port] properties: hostname: {type: string, minLength: 1, example: orders.service.internal, description: Name to resolve. Every returned address becomes one target.} record_types: {type: array, items: {type: string, enum: [a, aaaa]}, default: [a, aaaa], minItems: 1, description: Record types to query.} resolver: {type: string, nullable: true, example: '10.0.0.53:53', description: Resolver address. Null uses the system resolver.} - $ref: '#/components/schemas/DiscoveryConfigCommon' DnsSrvDiscoveryConfig: allOf: - type: object required: [service_name] properties: service_name: {type: string, minLength: 1, example: _http._tcp.orders.internal, description: "SRV owner name. Each record supplies a host, port, weight, and priority."} resolver: {type: string, nullable: true, example: '10.0.0.53:53', description: Resolver address. Null uses the system resolver.} use_record_weight: {type: boolean, default: true, description: Take the weight from the SRV record instead of config.weight.} use_record_priority: {type: boolean, default: true, description: "Take the priority from the SRV record instead of config.priority. SRV priority is already lowest-first, so it maps onto target priority unchanged."} - $ref: '#/components/schemas/DiscoveryConfigCommon' HttpDiscoveryConfig: allOf: - type: object required: [url] properties: url: {type: string, example: 'https://registry.internal/v1/orders/endpoints', description: Endpoint polled with GET. It must answer with JSON.} headers: {type: object, additionalProperties: {type: string}, default: {}, description: "Extra request headers, usually an authorization header. Stored in the database and returned to admins that can read the source."} endpoints_path: {type: string, nullable: true, example: data.endpoints, description: "Dot path to the endpoint array when the document does not return it at the root. Each array item is an object: host required, then optional port, weight, priority, enabled, key, and metadata. key becomes the target's provider_key."} tls_verify: {type: boolean, default: true, description: Verify the registry's TLS certificate. Leave on outside a lab.} - $ref: '#/components/schemas/DiscoveryConfigCommon' DiscoverySourceInput: type: object required: [name, provider, config] properties: name: {type: string, minLength: 1, example: orders-dns, description: "Operator-facing name, unique within the service."} provider: {$ref: '#/components/schemas/DiscoveryProviderId'} enabled: {type: boolean, default: true, description: A disabled source stops refreshing and marks its targets draining. They receive no new traffic and are deleted after removal_grace_ms.} config: description: Provider configuration. dns uses DnsDiscoveryConfig, dns-srv uses DnsSrvDiscoveryConfig, http uses HttpDiscoveryConfig. Scheduling, port, weight, and priority live here. oneOf: - $ref: '#/components/schemas/DnsDiscoveryConfig' - $ref: '#/components/schemas/DnsSrvDiscoveryConfig' - $ref: '#/components/schemas/HttpDiscoveryConfig' stale_after_ms: {type: integer, format: int64, minimum: 0, default: 300000, description: "How long the last known good endpoints stay trusted after refreshes start failing. Past it the source turns stale and its targets remain visible but stop taking new traffic. Zero marks them stale on the first failure."} removal_grace_ms: {type: integer, format: int64, minimum: 0, default: 60000, description: How long a target that disappeared from a successful refresh keeps its in-flight requests before deletion. It takes no new traffic during that window. Zero deletes it at once.} DiscoverySource: allOf: - type: object required: [id, service_id, created_at, updated_at] properties: id: {$ref: '#/components/schemas/Id'} service_id: {$ref: '#/components/schemas/Id'} created_at: {$ref: '#/components/schemas/Timestamp'} updated_at: {$ref: '#/components/schemas/Timestamp'} - $ref: '#/components/schemas/DiscoverySourceInput' DiscoverySourceStatus: type: object required: [source_id, state, last_attempt_at, last_success_at, next_refresh_at, revision, endpoint_count, active_count, draining_count, stale_count, last_error] properties: source_id: {$ref: '#/components/schemas/Id'} state: type: string example: healthy description: >- pending before the first refresh finishes, healthy after a refresh succeeded, failing while refreshes fail but the last known good endpoints are still within stale_after_ms, stale once that window has passed, and disabled when the source is turned off. last_attempt_at: {allOf: [{$ref: '#/components/schemas/Timestamp'}], nullable: true, description: "When the last refresh ran, successful or not."} last_success_at: {allOf: [{$ref: '#/components/schemas/Timestamp'}], nullable: true, description: When the endpoint set was last resolved successfully. It anchors stale_after_ms.} next_refresh_at: {allOf: [{$ref: '#/components/schemas/Timestamp'}], nullable: true, description: When the next scheduled refresh is due. Null while the source is disabled.} revision: {type: string, nullable: true, example: '7', description: "Opaque token for the applied endpoint set. It changes when a provider revision changes or the normalized endpoint set changes. Null before the first successful refresh."} endpoint_count: {type: integer, minimum: 0, description: Endpoints in the last applied result.} active_count: {type: integer, minimum: 0, description: Targets taking new traffic.} draining_count: {type: integer, minimum: 0, description: Targets inside the removal grace period.} stale_count: {type: integer, minimum: 0, description: Last known good targets kept past stale_after_ms.} last_error: {type: string, nullable: true, example: 'lookup timed out after 5000ms', description: Why the last attempt failed. Cleared by the next success.} RouteSplit: type: object required: [service_id, weight] properties: service_id: {$ref: '#/components/schemas/Id'} weight: {type: integer, format: int32, minimum: 1} RouteInput: type: object required: [name, service_id] properties: name: {type: string, minLength: 1, example: orders-api} service_id: {$ref: '#/components/schemas/Id'} priority: {type: integer, format: int32, default: 0} hosts: {type: array, default: [], items: {type: string}, example: [api.example.com, '*.internal.example.com']} paths: type: array default: [] items: {type: string} example: [/orders, '~/users/\d+'] description: Literal prefixes (`/orders`, longest match wins) or `~`-prefixed regexes matched from the start of the path (`~/users/\d+`). `strip_path` strips exactly the matched portion. methods: {type: array, default: [], items: {type: string}, example: [GET, POST]} headers: type: object default: {} additionalProperties: {type: string} description: Every header must match. Names are case-insensitive; `*` means present with any value. splits: type: array default: [] items: {$ref: '#/components/schemas/RouteSplit'} description: Non-empty weighted splits override `service_id`. strip_path: {type: boolean, default: false} preserve_host: {type: boolean, default: false} enabled: {type: boolean, default: true} Route: allOf: - type: object required: [id, name, service_id, priority, hosts, paths, methods, headers, splits, strip_path, preserve_host, enabled] properties: {id: {$ref: '#/components/schemas/Id'}} - $ref: '#/components/schemas/RouteInput' StreamRouteInput: type: object required: [name, listen_addr, service_id] properties: name: {type: string, minLength: 1, example: postgres} listen_addr: {type: string, example: '0.0.0.0:5432'} service_id: {$ref: '#/components/schemas/Id'} enabled: {type: boolean, default: true} StreamRoute: allOf: - type: object required: [id, name, listen_addr, service_id, enabled] properties: {id: {$ref: '#/components/schemas/Id'}} - $ref: '#/components/schemas/StreamRouteInput' StreamRouteResult: allOf: - $ref: '#/components/schemas/StreamRoute' - type: object properties: note: {type: string, enum: [listener changes take effect on restart]} StreamRouteDeleteResult: allOf: - $ref: '#/components/schemas/DeleteResult' - type: object required: [note] properties: note: {type: string, enum: [listener changes take effect on restart]} PluginInput: type: object required: [type, scope] properties: type: {$ref: '#/components/schemas/PluginType'} scope: {$ref: '#/components/schemas/PluginScope'} service_id: {$ref: '#/components/schemas/NullableId'} route_id: {$ref: '#/components/schemas/NullableId'} config: $ref: '#/components/schemas/PluginConfig' ordering: {type: integer, format: int32, default: 0} enabled: {type: boolean, default: true} description: "`service_id` is required for service scope; `route_id` is required for route scope." Plugin: allOf: - type: object required: [id, type, scope, service_id, route_id, config, ordering, enabled] properties: {id: {$ref: '#/components/schemas/Id'}} - $ref: '#/components/schemas/PluginInput' NullableId: type: integer format: int64 minimum: 1 nullable: true PluginConfig: description: Config shape is selected by the sibling plugin `type`; unknown fields are ignored by the runtime. oneOf: - $ref: '#/components/schemas/KeyAuthConfig' - $ref: '#/components/schemas/BasicAuthConfig' - $ref: '#/components/schemas/JwtConfig' - $ref: '#/components/schemas/AclConfig' - $ref: '#/components/schemas/IpRestrictionConfig' - $ref: '#/components/schemas/RateLimitConfig' - $ref: '#/components/schemas/ProxyCacheConfig' - $ref: '#/components/schemas/RequestSizeLimitConfig' - $ref: '#/components/schemas/RequestTerminationConfig' - $ref: '#/components/schemas/RedirectConfig' - $ref: '#/components/schemas/HstsConfig' - $ref: '#/components/schemas/CorsConfig' - $ref: '#/components/schemas/WasmPluginConfig' - $ref: '#/components/schemas/TransformConfig' - $ref: '#/components/schemas/ResponseBodyTransformConfig' - $ref: '#/components/schemas/HttpLogConfig' KeyAuthConfig: type: object properties: key_names: {type: array, items: {type: string}, default: [apikey, x-api-key]} hide_credentials: {type: boolean, default: false} BasicAuthConfig: type: object properties: realm: {type: string, default: Raahi} JwtConfig: type: object properties: key_claim_name: {type: string, default: iss} uri_param_names: {type: array, items: {type: string}, default: [jwt]} require_exp: {type: boolean, default: false} jwks_url: {type: string, format: uri, nullable: true} consumer_claim: {type: string, default: sub} AclConfig: type: object properties: allow: {type: array, items: {type: string}, default: []} deny: {type: array, items: {type: string}, default: []} IpRestrictionConfig: type: object properties: allow: {type: array, items: {type: string}, default: []} deny: {type: array, items: {type: string}, default: []} status: {type: integer, minimum: 100, maximum: 599, default: 403} message: {type: string, default: Your IP address is not allowed} RateLimitConfig: type: object properties: limit: {type: integer, format: int32, minimum: 0, default: 60} window_secs: {type: integer, format: int64, minimum: 0, default: 60} key: {type: string, enum: [ip, consumer, route], default: ip} headers: {type: boolean, default: true} ProxyCacheConfig: description: >- Shared cache for anonymous unconditional GET requests only. Authentication, cookies, range/conditional requests and request cache directives bypass it. Responses with Set-Cookie, Vary, Expires, private, no-cache or no-store are not stored. max-age/s-maxage and Age cap freshness. Cache content has a global 64 MiB budget and is invalidated on configuration reload. type: object properties: ttl_secs: {type: integer, format: int64, minimum: 1, default: 60} max_body_bytes: {type: integer, format: int64, minimum: 0, default: 1048576} methods: {type: array, items: {type: string}, default: [GET], description: "Eligible methods; only GET is currently supported for safe shared caching."} cache_key_query: {type: boolean, default: true} RequestSizeLimitConfig: type: object properties: max_bytes: {type: integer, format: int64, minimum: 0, default: 10485760} require_content_length: {type: boolean, default: false} RequestTerminationConfig: type: object properties: status: {type: integer, minimum: 100, maximum: 599, default: 503} message: {type: string, default: Service temporarily unavailable} content_type: {type: string, default: 'text/plain; charset=utf-8'} RedirectConfig: type: object required: [location] properties: status: {type: integer, enum: [301, 302, 307, 308], default: 302} location: {type: string, format: uri} preserve_path: {type: boolean, default: true} http_only: {type: boolean, default: false} HstsConfig: type: object properties: max_age_secs: {type: integer, format: int64, minimum: 1, default: 63072000} include_subdomains: {type: boolean, default: false} preload: {type: boolean, default: false} CorsConfig: type: object properties: allow_origins: {type: array, items: {type: string}, default: ['*']} allow_methods: {type: array, items: {type: string}, default: [GET, POST, PUT, PATCH, DELETE, OPTIONS]} allow_headers: {type: array, items: {type: string}, default: ['*']} expose_headers: {type: array, items: {type: string}, default: []} allow_credentials: {type: boolean, default: false} max_age: {type: integer, format: int32, minimum: 0, default: 3600} WasmPluginConfig: type: object required: [module] properties: module: {type: string, minLength: 1} config: {type: object, default: {}, additionalProperties: true} fuel: {type: integer, format: int64, minimum: 0, default: 100000000} TransformConfig: type: object properties: add: {type: object, default: {}, additionalProperties: {type: string}} remove: {type: array, items: {type: string}, default: []} ResponseBodyTransformConfig: type: object required: [replace] properties: replace: type: array minItems: 1 items: type: object required: [from, to] properties: {from: {type: string}, to: {type: string}} max_body_bytes: {type: integer, format: int64, minimum: 0, default: 1048576} content_types: {type: array, items: {type: string}, default: [text/, application/json]} HttpLogConfig: type: object required: [endpoint] properties: endpoint: {type: string, format: uri} headers: {type: object, default: {}, additionalProperties: {type: string}} batch_max: {type: integer, minimum: 0, default: 50} flush_interval_ms: {type: integer, format: int64, minimum: 0, default: 2000} ConsumerInput: type: object required: [username] properties: username: {type: string, minLength: 1, example: mobile-app} groups: {type: array, items: {type: string}, default: []} Consumer: allOf: - type: object required: [id, username, groups] properties: {id: {$ref: '#/components/schemas/Id'}} - $ref: '#/components/schemas/ConsumerInput' CredentialInput: type: object required: [type, identifier] properties: type: {$ref: '#/components/schemas/CredentialType'} identifier: {type: string, minLength: 1, description: 'API key, Basic username, or JWT key-claim value.'} secret: {type: string, writeOnly: true, description: Required for basic-auth and JWT; unused for key-auth.} algorithm: {type: string, enum: [HS256, HS384, HS512, RS256], default: HS256, description: JWT only.} Credential: type: object required: [id, consumer_id, type, identifier] properties: id: {$ref: '#/components/schemas/Id'} consumer_id: {$ref: '#/components/schemas/Id'} type: {$ref: '#/components/schemas/CredentialType'} identifier: {type: string} CertificateInput: type: object required: [name] description: Provide cert_pem and key_pem for a manual certificate, or acme_config for automatic management. properties: name: {type: string, minLength: 1} sni: {type: array, items: {type: string}, default: [], example: [api.example.com, '*.example.com']} cert_pem: {type: string, description: PEM certificate chain.} key_pem: {type: string, writeOnly: true, description: Matching PEM private key.} acme_config: {$ref: '#/components/schemas/AcmeConfig'} AcmeAccountStatus: type: object required: [configured, account_registered] properties: configured: {type: boolean, description: EAB registration credentials are saved.} account_registered: {type: boolean, description: A reusable ACME account is already registered.} AcmeEabCredentials: type: object additionalProperties: false required: [directory_url, key_id, hmac_key] properties: directory_url: {type: string, example: zerossl} key_id: {type: string, writeOnly: true, minLength: 1, maxLength: 1024} hmac_key: {type: string, writeOnly: true, minLength: 1, maxLength: 4096, description: Base64url-encoded HMAC key, with optional padding.} AcmeBackupState: type: object nullable: true description: Present only in secret exports. These fields are restored by import. properties: cloudflare_api_token: {type: string, nullable: true} accounts: type: array items: type: object required: [directory_url, credentials] properties: directory_url: {type: string} email: {type: string, nullable: true} credentials: {type: string, description: Serialized ACME account credentials.} eab_credentials: type: array default: [] items: type: object required: [directory_url, key_id, hmac_key] properties: directory_url: {type: string} key_id: {type: string} hmac_key: {type: string} AcmeConfig: type: object required: [challenge] properties: directory_url: {type: string, default: production, description: "production (default), staging, zerossl, or a custom HTTPS ACME directory URL. Existing aliases remain supported."} challenge: {type: string, enum: [dns-01, tls-alpn-01], description: "ZeroSSL uses dns-01 in Raahi. DNS-01 currently uses Cloudflare."} email: {type: string, format: email, nullable: true} AcmeStatus: type: object required: [state] properties: state: {type: string, enum: [pending, issuing, issued, failed]} issued_at: {type: string, format: date-time, nullable: true} expires_at: {type: string, format: date-time, nullable: true} last_attempt: {type: string, format: date-time, nullable: true} last_error: {type: string, nullable: true} Certificate: type: object required: [id, name, sni, cert_pem] properties: id: {$ref: '#/components/schemas/Id'} name: {type: string} sni: {type: array, items: {type: string}} cert_pem: {type: string} acme_config: {$ref: '#/components/schemas/AcmeConfig'} acme_status: {$ref: '#/components/schemas/AcmeStatus'} SettingsInput: type: object required: [proxy_http_addr, admin_addr] properties: proxy_http_addr: {type: string, example: '0.0.0.0:8080'} proxy_https_addr: {type: string, nullable: true, example: '0.0.0.0:8443'} admin_addr: {type: string, example: '127.0.0.1:9080'} default_lb: {$ref: '#/components/schemas/LoadBalancingAlgorithm'} active_certificate_id: {$ref: '#/components/schemas/NullableId'} Settings: allOf: - type: object required: [proxy_http_addr, proxy_https_addr, admin_addr, default_lb, active_certificate_id] - $ref: '#/components/schemas/SettingsInput' WasmModuleInput: type: object required: [name] properties: name: {type: string, minLength: 1} description: {type: string, default: ''} wasm_base64: {type: string, format: byte, writeOnly: true} wat: {type: string, writeOnly: true, description: WebAssembly text source.} WasmModule: type: object required: [id, name, description, size_bytes, created_at] properties: id: {$ref: '#/components/schemas/Id'} name: {type: string} description: {type: string} size_bytes: {type: integer, format: int64, minimum: 0} created_at: {$ref: '#/components/schemas/Timestamp'} PurgeResult: type: object required: [purged] properties: purged: {type: integer, format: int64, minimum: 0} RouteHit: type: object required: [route_id, count, errors, avg_latency_ms] properties: route_id: {$ref: '#/components/schemas/Id'} count: {type: integer, format: int64, minimum: 0} errors: {type: integer, format: int64, minimum: 0} avg_latency_ms: {type: number, format: double, minimum: 0} ConsumerHit: type: object required: [consumer, count] properties: consumer: {type: string} count: {type: integer, format: int64, minimum: 0} MetricsSnapshot: type: object required: [total, class_2xx, class_3xx, class_4xx, class_5xx, no_route, avg_latency_ms, p50_latency_ms, p95_latency_ms, p99_latency_ms, top_routes, top_consumers] properties: total: {type: integer, format: int64, minimum: 0} class_2xx: {type: integer, format: int64, minimum: 0} class_3xx: {type: integer, format: int64, minimum: 0} class_4xx: {type: integer, format: int64, minimum: 0} class_5xx: {type: integer, format: int64, minimum: 0} no_route: {type: integer, format: int64, minimum: 0} avg_latency_ms: {type: number, format: double, minimum: 0} p50_latency_ms: {type: number, format: double, minimum: 0} p95_latency_ms: {type: number, format: double, minimum: 0} p99_latency_ms: {type: number, format: double, minimum: 0} top_routes: {type: array, items: {$ref: '#/components/schemas/RouteHit'}} top_consumers: {type: array, items: {$ref: '#/components/schemas/ConsumerHit'}} RequestRecord: type: object required: [ts_ms, method, host, path, status, latency_ms, route_id, service_id, upstream, consumer] properties: ts_ms: {type: integer, format: int64, minimum: 0, description: Unix timestamp in milliseconds.} method: {type: string} host: {type: string} path: {type: string} status: {type: integer, minimum: 100, maximum: 599} latency_ms: {type: number, format: double, minimum: 0} route_id: {$ref: '#/components/schemas/NullableId'} service_id: {$ref: '#/components/schemas/NullableId'} upstream: {type: string, nullable: true} consumer: {type: string, nullable: true} ConfigSummary: type: object required: [version, routes, services, plugins, consumers, key_credentials] properties: version: {type: integer, format: int64, minimum: 0} routes: {type: integer, minimum: 0} services: {type: integer, minimum: 0} plugins: {type: integer, minimum: 0} consumers: {type: integer, minimum: 0} key_credentials: {type: integer, minimum: 0} TargetHealth: type: object required: [service_id, target_id, host, port, healthy] properties: service_id: {$ref: '#/components/schemas/Id'} target_id: {$ref: '#/components/schemas/Id'} host: {type: string} port: {type: integer, minimum: 0, maximum: 65535} healthy: {type: boolean} RouterTestSplit: type: object required: [service_id, service_name, weight] properties: service_id: {$ref: '#/components/schemas/Id'} service_name: {type: string} weight: {type: integer, format: int32, minimum: 1} RouterTestResult: type: object required: [matched] properties: matched: {type: boolean} route_id: {$ref: '#/components/schemas/Id'} route_name: {type: string} service_id: {$ref: '#/components/schemas/Id'} service_name: {type: string} matched_prefix: {type: string} strip_path: {type: boolean} preserve_host: {type: boolean} plugins: {type: array, items: {$ref: '#/components/schemas/PluginType'}} splits: {type: array, items: {$ref: '#/components/schemas/RouterTestSplit'}} ExportService: type: object required: [service, targets] properties: service: {$ref: '#/components/schemas/Service'} targets: {type: array, items: {$ref: '#/components/schemas/Target'}} ExportConsumer: type: object required: [consumer, credentials] properties: consumer: {$ref: '#/components/schemas/Consumer'} credentials: type: array items: allOf: - $ref: '#/components/schemas/Credential' - type: object properties: secret: {type: string, nullable: true, writeOnly: true, description: Present only when include_secrets=true.} ExportCertificate: allOf: - $ref: '#/components/schemas/Certificate' - type: object properties: key_pem: {type: string, writeOnly: true, description: Present only when include_secrets=true.} ExportWasmModule: type: object required: [name, description, wasm_base64] properties: name: {type: string} description: {type: string} wasm_base64: {type: string, format: byte} ExportDiscoverySource: type: object required: [service_id, source] properties: service_id: {$ref: '#/components/schemas/Id'} source: {$ref: '#/components/schemas/DiscoverySource'} ExportDocument: type: object required: [raahi_export_version, settings, services, discovery_sources, routes, stream_routes, plugins, consumers, certificates, wasm_modules] properties: acme: {$ref: '#/components/schemas/AcmeBackupState'} raahi_export_version: {type: integer, enum: [1]} settings: {$ref: '#/components/schemas/Settings'} services: {type: array, items: {$ref: '#/components/schemas/ExportService'}} discovery_sources: {type: array, items: {$ref: '#/components/schemas/ExportDiscoverySource'}} routes: {type: array, items: {$ref: '#/components/schemas/Route'}} stream_routes: {type: array, items: {$ref: '#/components/schemas/StreamRoute'}} plugins: {type: array, items: {$ref: '#/components/schemas/Plugin'}} consumers: {type: array, items: {$ref: '#/components/schemas/ExportConsumer'}} certificates: {type: array, items: {$ref: '#/components/schemas/ExportCertificate'}} wasm_modules: {type: array, items: {$ref: '#/components/schemas/ExportWasmModule'}} ImportDocument: allOf: - $ref: '#/components/schemas/ExportDocument' description: Export-shaped document. Entity IDs are remapped. Redacted certificates and dangling references may be skipped. ConfigDocument: type: object additionalProperties: false required: [raahi_config] properties: raahi_config: {type: integer, enum: [1]} services: {type: array, items: {$ref: '#/components/schemas/ConfigService'}} routes: {type: array, items: {$ref: '#/components/schemas/ConfigRoute'}} stream_routes: type: array items: type: object additionalProperties: false required: [name, listen_addr, service] properties: name: {type: string} listen_addr: {type: string, example: '0.0.0.0:5432'} service: {type: string, description: Service name.} enabled: {type: boolean, default: true} plugins: type: array description: Global plugins. items: {$ref: '#/components/schemas/ConfigPlugin'} consumers: type: array items: type: object additionalProperties: false required: [username] properties: username: {type: string} groups: {type: array, items: {type: string}} credentials: type: array items: type: object additionalProperties: false required: [type, identifier] properties: type: {type: string, enum: [key-auth, basic-auth, jwt]} identifier: {type: string} secret: {type: string, description: Omit to keep the stored secret of an existing credential.} algorithm: {type: string, enum: [HS256, HS384, HS512, RS256]} certificates: type: array items: type: object additionalProperties: false required: [name] description: Set acme_config for ACME, or cert_pem and key_pem for an uploaded certificate. If both PEMs are left out, an existing certificate keeps its stored ones. properties: name: {type: string} sni: {type: array, items: {type: string}} cert_pem: {type: string} key_pem: {type: string} acme_config: {$ref: '#/components/schemas/AcmeConfig'} ConfigService: type: object additionalProperties: false required: [name] properties: name: {type: string} kind: {type: string, enum: [proxy, static], default: proxy} root: {type: string, description: Absolute directory on the Raahi host for static services.} spa_fallback: {type: boolean, default: false} protocol: {type: string, enum: [http, https], default: http} connect_timeout_ms: {type: integer, default: 5000} read_timeout_ms: {type: integer, default: 60000} write_timeout_ms: {type: integer, default: 60000} retries: {type: integer, default: 1} lb_algorithm: {type: string, enum: [round_robin, random, consistent, weighted], default: round_robin} upstream_authority: {type: string} tls_sni: {type: string} health_path: {type: string} targets: type: array description: Manual targets. Apply matches them by host and port. items: type: object additionalProperties: false required: [host, port] properties: host: {type: string} port: {type: integer, minimum: 1, maximum: 65535} weight: {type: integer, default: 100} priority: {type: integer, default: 0} enabled: {type: boolean, default: true} discovery: type: array items: type: object additionalProperties: false required: [name, provider] properties: name: {type: string} provider: {type: string} config: {type: object, additionalProperties: true} enabled: {type: boolean, default: true} stale_after_ms: {type: integer, default: 300000} removal_grace_ms: {type: integer, default: 60000} plugins: type: array description: Service-scoped plugins. items: {$ref: '#/components/schemas/ConfigPlugin'} ConfigRoute: type: object additionalProperties: false required: [name, service] properties: name: {type: string} service: {type: string, description: Service name.} priority: {type: integer, default: 0} hosts: {type: array, items: {type: string}} paths: {type: array, items: {type: string}} methods: {type: array, items: {type: string}} headers: {type: object, additionalProperties: {type: string}} splits: type: array items: type: object additionalProperties: false required: [service, weight] properties: service: {type: string} weight: {type: integer, minimum: 1} strip_path: {type: boolean, default: false} preserve_host: {type: boolean, default: false} enabled: {type: boolean, default: true} plugins: type: array description: Route-scoped plugins. items: {$ref: '#/components/schemas/ConfigPlugin'} ConfigPlugin: type: object additionalProperties: false required: [type] description: Apply matches plugins by type and position among the plugins of the same owner. properties: type: {type: string, example: key-auth} config: {type: object, additionalProperties: true} ordering: {type: integer, default: 0} enabled: {type: boolean, default: true} ApplyReport: type: object required: [dry_run, changes] properties: dry_run: {type: boolean} changes: type: array items: type: object required: [action, kind, name] properties: action: {type: string, enum: [create, update, delete]} kind: {type: string, enum: [service, target, discovery_source, route, stream_route, plugin, consumer, credential, certificate]} name: {type: string, example: key-auth on route billing} note: {type: string, description: Set when stream routes changed. Listener changes take effect on restart.} ImportReport: type: object required: [services, targets, discovery_sources, routes, stream_routes, plugins, consumers, credentials, certificates, wasm_modules, skipped] properties: services: {type: integer, minimum: 0} targets: {type: integer, minimum: 0} discovery_sources: {type: integer, minimum: 0} routes: {type: integer, minimum: 0} stream_routes: {type: integer, minimum: 0} plugins: {type: integer, minimum: 0} consumers: {type: integer, minimum: 0} credentials: {type: integer, minimum: 0} certificates: {type: integer, minimum: 0} wasm_modules: {type: integer, minimum: 0} skipped: {type: array, items: {type: string}}