# -------------------------------------------------------------------- # Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). # # WSO2 LLC. licenses this file to you under the Apache License, # Version 2.0 (the "License"); you may not use this file except # in compliance with the License. You may obtain a copy of the # License at http://www.apache.org/licenses/LICENSE-2.0 # -------------------------------------------------------------------- # # Platform API configuration template # # Copy this file to your deployment's config location and edit the values. # Read by the Platform API (Go binary). Lists EVERY key the Platform API # reads, so it doubles as the configuration reference — every key below is # already at its built-in default, so deleting a key just restores that # default. Ships with distributions (e.g. the AI Workspace zip), copied at # build time from here. # # Every key below is a plain literal — edit the value directly. For secrets # (encryption_key, admin credentials, the webhook secret) that means filling # in a real value before deploying; an empty literal is a placeholder, not a # safe default. For a value that should come from the environment or a # mounted file instead, use an interpolation token: # # key = '{{ env "APIP_CP_VAR" "default" }}' # key = '{{ file "/secrets/platform-api/key" }}' # # Both fail closed at startup — a missing file, a file outside the allowed # source directories (default /etc/platform-api and /secrets/platform-api; # override with APIP_CONFIG_FILE_SOURCE_ALLOWLIST), or an unset variable with # no default all refuse to start rather than run with an empty value. # # Never write a secret as a raw literal in a file committed to version # control or hardcode one in docker-compose.yaml. # # QUICK START (file auth mode — no external IDP needed): # 1. Copy this file to config.toml (standalone), or merge its [platform_api] # section into the unified config.toml alongside other components. # 2. Generate the at-rest encryption key into a mounted file: openssl rand -hex 32 # > /etc/platform-api/keys/encryption.key. The field below reads it via a # {{ file "..." }} token — never paste raw key values into this file. # 3. Generate an RS256 JWT keypair and mount it at the paths named by # auth.jwt.public_key_file / private_key_file below: # openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \ # -out jwt_private.pem # openssl rsa -in jwt_private.pem -pubout -out jwt_public.pem # 4. Set [platform_api.auth] mode = "file" and configure users. # 5. Run: docker compose up # # OIDC mode: set [platform_api.auth] mode = "idp" and configure [platform_api.auth.idp]. # -------------------------------------------------------------------- # --------------------------------------------------------------------------- # All Platform API settings live under [platform_api] / [platform_api.*]. # --------------------------------------------------------------------------- [platform_api] # Resource paths loaded at startup. Defaults match the published container # image — override only for a custom layout. db_schema_path = "./internal/database/schema.sql" openapi_spec_path = "./resources/openapi.yaml" llm_template_definitions_path = "./resources/default-llm-provider-templates" # Byte cap when fetching a remote OpenAPI spec by URL. <= 0 uses the # built-in 5 MiB default. openapi_spec_max_fetch_bytes = 5242880 [platform_api.logging] # Level is matched case-insensitively; lowercase is canonical. level = "info" # debug | info | warn | error # Log encoding. Use "json" when shipping logs to an aggregator. format = "text" # text | json # --------------------------------------------------------------------------- # Security # --------------------------------------------------------------------------- [platform_api.security] # Single 32-byte key (64 hex chars or base64) used for ALL at-rest # encryption (secrets, subscription tokens, WebSub HMAC secrets). # REQUIRED — an empty value fails config load. Generate with: # openssl rand -hex 32 # Prefer a mounted secret file in production: # encryption_key = '{{ file "/secrets/platform-api/encryption_key" }}' encryption_key = "" [platform_api.security.api_key] # Accepted API key hashing algorithms (comma-separated). sha256 is the # default; add "sha512" to accept both. hashing_algorithms = "sha256" # --------------------------------------------------------------------------- # Database # --------------------------------------------------------------------------- [platform_api.database] driver = "sqlite3" # "sqlite3" | "postgres" | "postgresql" | "pgx" | "sqlserver" | "mssql" # SQLite — ignored when driver = "postgres". path = "/app/data/api_platform.db" # PostgreSQL/SQL Server — ignored when driver = "sqlite3". host, port, name, # and user are required for every non-sqlite3 driver, or config load fails. host = "localhost" port = 5432 name = "platform_api" user = "platform_api" # Prefer a mounted secret file in production, e.g. # password = '{{ file "/secrets/platform-api/postgres_password" }}' # The "" default here is safe because sqlite3 needs no password; set a real # value when driver is a non-sqlite3 database. password = "" ssl_mode = "disable" # "disable" | "require" | "verify-ca" | "verify-full" # CA cert used to verify the server's certificate. Required when ssl_mode # is "verify-ca" or "verify-full"; ignored otherwise. ssl_root_cert = "" # Client cert/key pair for mutual TLS (PostgreSQL only — SQL Server's driver # has no client-certificate support). Both must be set together or both # left empty. ssl_cert = "" ssl_key = "" # Connection pool — tune for production workloads. max_open_conns = 25 # maximum open connections max_idle_conns = 10 # maximum idle connections in the pool conn_max_lifetime = 300 # seconds before a connection is recycled # --------------------------------------------------------------------------- # Authentication # # auth.mode selects exactly one mode: # "internal_token" — verify asymmetrically-signed (RS256) JWTs minted by # another trusted platform component, signed with the # matching RSA private key; verified here against # auth.jwt.public_key_file. # "file" — internal_token + local username/password login: the # login endpoint authenticates from # [platform_api.auth.file] and issues RS256 JWTs signed # with auth.jwt.private_key_file. # "idp" — validate tokens against an external IDP's JWKS # ([platform_api.auth.idp]). # Only the selected mode's section is read; the others are ignored. # --------------------------------------------------------------------------- [platform_api.auth] mode = "file" # Authorization — how a verified token's privileges are checked. Independent of # the authentication mode above: these settings apply whether the token was # verified against an IDP's JWKS or with a local public key, because an # enterprise-IDP-minted token carries the same roles claim either way. [platform_api.auth.authorization] # Enforce per-endpoint OAuth2 scopes on validated tokens. Set false only to # temporarily bypass authorization during development. enabled = true # "scope" (default) checks the scope claim; "role" checks the roles claim # configured below at claim_mappings.roles, expanding each role via role_to_scope_mapping. mode = "scope" # Path to a YAML file mapping role names to platform scopes. Required when # mode = "role" (startup fails if unset), and also when any file-mode user below # names a role — the login endpoint expands that role through this same file. # The packs mount their editable sample at /etc/platform-api/role-to-scope-mapping.yaml. role_to_scope_mapping = "" # JWT claim name mappings — shared by all three auth modes ("idp" reads # incoming claims by these names; "file" mode's login endpoint signs tokens # using these names; "internal_token" mode reads tokens minted elsewhere by # these names too), so issuance and validation never drift apart. Each value # is either a flat top-level claim name ("org_id") or a dot-separated path # into a nested claim ("realm_access.org_id") — useful for IDPs like # Keycloak that nest fields such as roles under realm_access.roles. [platform_api.auth.claim_mappings] organization = "organization" # claim carrying the org ID org_name = "org_name" # claim carrying the org display name org_handle = "org_handle" # claim carrying the org URL slug user_id = "sub" # claim used as the user's unique ID username = "username" email = "email" scope = "scope" # space-separated scope string # Claim carrying the user's roles. Read in role authorization mode, and it is the # claim the file-mode login endpoint signs the user's roles into. Default suits # Asgardeo/Entra ID; Keycloak nests it: "realm_access.roles". A dotted path works # in both directions — file mode signs the nested object the reader resolves — so # the same mapping serves whichever auth mode is active. roles = "roles" # IDP (JWKS-based) — used when mode = "idp" (Asgardeo, Keycloak, Auth0, etc.). # jwks_url and issuer are required in that mode. [platform_api.auth.idp] name = "asgardeo" # friendly name for logging jwks_url = "https://accounts.example.com/oauth2/jwks" issuer = ["https://accounts.example.com"] # list of accepted issuers audience = [] # accepted "aud" values; empty = skip audience check # File auth — local username/password login, used when mode = "file". Ideal # for initial / air-gapped setup; not recommended for production — prefer # an IDP. [platform_api.auth.file.organization] id = "default" # Required: organization handle (URL-safe slug) display_name = "Default" region = "us" # Platform organization UUID, emitted as the `organization` claim in issued # tokens. Pin it to keep the organization stable across fresh databases. uuid = "99089a17-72e0-4dd8-a2f4-c8dfbb085295" # Add one [[platform_api.auth.file.users]] block per user. # Generate a bcrypt hash with: htpasswd -bnBC 12 "" | tr -d ':\n' [[platform_api.auth.file.users]] # REQUIRED — an empty username/password_hash fails config load rather than # starting with a blank or guessable credential. username = "" password_hash = "" # REQUIRED — one or more roles from the auth.authorization.role_to_scope_mapping # file above, and this user's entire grant. The login endpoint expands them into # the token's scope claim (the union of what each grants — most-permissive wins) # and also emits the roles themselves as the roles claim, so the same token works # whether auth.authorization.mode is "scope" (default) or "role". A user with no # roles, or one naming a role the mapping file doesn't define, fails startup rather # than logging in successfully and then being denied every request. # # There is no per-user scope list: what a role grants is defined once, in the # mapping file, so no user can drift out of step with the roles it names. To grant # something no shipped role covers, name several roles, or add a role to that file # (see resources/role-to-scope-mapping.yaml for the shipped ones and the scope # namespaces it may use). # # Left empty here because role_to_scope_mapping above is empty in this template — set that # first, then name a role. roles = [] # Additional users — uncomment the WHOLE block (including the [[...]] header) # and replace the placeholder hash with a real bcrypt hash before use. # [[platform_api.auth.file.users]] # username = "readonly" # password_hash = "$2a$12$" # roles = ["ap_viewer"] # JWT (local RS256) — used by "internal_token" and "file" modes. Tokens are # signed asymmetrically: "internal_token" only verifies tokens minted elsewhere # with the public key; "file" also signs the tokens its login endpoint issues # with the private key. Symmetric (HMAC) and unsigned ("none") tokens are # rejected. Keys are mounted PEM files, referenced here by path only — the # server reads and parses them itself at startup/use, so the PEM content is # never inlined into config (scripts/setup.sh generates the pair). Mount your # keys at these paths, or point elsewhere by editing the path directly. [platform_api.auth.jwt] issuer = "platform-api" # public_key_file is REQUIRED in every mode (verifies token signatures); # private_key_file is REQUIRED only in "file" mode (signs login tokens) and # must be the matching half of the pair. Neither is generated — a missing # path, unreadable file, or malformed key fails config load. Both files must # contain a PEM-encoded RSA key. public_key_file = "/etc/platform-api/keys/jwt_public.pem" private_key_file = "/etc/platform-api/keys/jwt_private.pem" # Lifetime of tokens issued by the file-mode login endpoint (Go duration # syntax). Not used for "internal_token" tokens — their expiry is whatever # "exp" claim the issuer set. token_ttl = "1h" # --------------------------------------------------------------------------- # Server listeners (HTTP + HTTPS) # --------------------------------------------------------------------------- # The plain-HTTP and HTTPS listeners are independent — enable either or both, # each on its own port. Serve plain HTTP internally, HTTPS externally, or run # both at once (e.g. to migrate clients between them without downtime). # # Defaults: the HTTPS listener is on at 9243; the plain-HTTP listener is off. # Enable the plain-HTTP listener only when a trusted upstream (ingress, # service-mesh sidecar) terminates TLS, or for internal traffic — never # expose it directly to untrusted networks. # # cert_file / key_file must point at a certificate pair when # server.https.enabled = true. Certificates are always required — there is # no self-signed fallback. setup.sh generates a pair for the quickstart. [platform_api.server.http] enabled = false port = 9080 [platform_api.server.https] enabled = true port = 9243 cert_file = "/app/data/certs/cert.pem" # default: ./data/certs/cert.pem key_file = "/app/data/certs/key.pem" # default: ./data/certs/key.pem # --------------------------------------------------------------------------- # Listener timeouts # --------------------------------------------------------------------------- # Bound the lifetime of a connection so a slow or idle peer cannot hold one # open indefinitely (Slowloris). These apply to both listeners, which serve # the same handler. Values are durations, e.g. "10s", "2m". # # 0 disables a timeout (net/http semantics). Disabling read or read_header # removes the Slowloris protection — only do so behind a proxy that enforces # its own bounds. Keep `write` generous: it bounds handler execution, and # some handlers proxy slow upstreams (LLM completions, deployments). # # WebSocket routes are unaffected — the deadlines are cleared on upgrade. [platform_api.server.timeouts] read_header = "10s" read = "60s" write = "120s" idle = "120s" # --------------------------------------------------------------------------- # CORS # --------------------------------------------------------------------------- # Origins allowed to make credentialed cross-origin requests, e.g. the # API Portal and AI Workspace origins. Must never contain "*"; leave # empty to disable cross-origin access (the default). [platform_api.server.cors] allowed_origins = "" # comma-separated, e.g. "https://workspace.example.com,https://devportal.example.com" # --------------------------------------------------------------------------- # WebSocket # --------------------------------------------------------------------------- [platform_api.server.websocket] max_connections = 1000 # global WebSocket connection limit connection_timeout = 30 # seconds before an idle connection is closed rate_limit_per_min = 1000 # maximum messages per minute per connection metrics_log_enabled = true # emit WebSocket metrics to the log metrics_log_interval = 10 # seconds between metrics log lines # --------------------------------------------------------------------------- # Gateway # --------------------------------------------------------------------------- [platform_api.gateway] # Reject gateway registrations whose runtime version does not match the expected range. enable_version_verification = false # Reject gateways that report an unexpected functionality type. enable_functionality_type_verification = false # --------------------------------------------------------------------------- # Deployments # --------------------------------------------------------------------------- [platform_api.deployments] max_per_api_gateway = 20 # maximum API deployments per gateway # Deployment timeout — mark stuck deployments as failed after timeout_duration seconds. timeout_enabled = true timeout_interval = 20 # seconds between timeout check sweeps timeout_duration = 60 # seconds before a stuck deployment is timed out # --------------------------------------------------------------------------- # EventHub — multi-replica HA event delivery # --------------------------------------------------------------------------- # Values are durations, e.g. "3s", "10m", "1h". All must be positive. [platform_api.event_hub] poll_interval = "3s" # how often each replica polls for new events cleanup_interval = "10m" # how often delivered events are purged retention_period = "1h" # how long delivered events are retained # --------------------------------------------------------------------------- # Webhook — control-plane webhook receiver # --------------------------------------------------------------------------- # The API Portal delivers signed events (API key / subscription # changes) to this endpoint. [platform_api.webhook] enabled = false # Shared secret with the API Portal, used both to verify request # signatures (HMAC-SHA256) and to derive the AES key that decrypts encrypted # payload fields (API key generate/regenerate, subscription token). Must match # the secret configured on the API Portal's webhook subscriber. REQUIRED # when enabled. The "" default below is only ever read while enabled = false. secret = "" signature_tolerance = "5m" # max age of a signed request (replay protection) max_body_size = 1048576 # request body cap in bytes (1 MiB) signature_header = "X-Api-Portal-Signature" # header carrying the "t=...,v1=..." signature