docs/openapi.yaml
Rama main · réplica de lectura del repositorio original.
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}}