Skip to content

Configurations

The API Portal & MCP Hub reads its configuration from configs/config.toml, layered over built-in defaults (src/config/configDefaults.js). This page explains how that file is loaded, how environment values are injected through interpolation tokens, and gives the full reference of every supported key.

How configuration is loaded

Precedence, lowest to highest:

  1. Built-in defaults (src/config/configDefaults.js)
  2. configs/config.toml, with any {{ env }} / {{ file }} references resolved

configs/config-template.toml documents every supported key with its default value as plain literals—a reference copy, not the file the portal actually reads.

Environment variables do not override config keys directly

There is no automatic APIP_AP_* prefix that maps environment variables onto config keys. An environment value reaches a setting only through an explicit interpolation token written into config.toml, resolved when the file loads. A field with no token always takes its literal TOML value (or the built-in default).

Interpolation tokens

Token Behavior
{{ env "NAME" }} Substitutes the value of environment variable NAME. Fails closed (aborts startup) if NAME is unset or empty—it does not fall through to a default.
{{ env "NAME" "default" }} Substitutes NAME's value if set and non-empty, else the literal "default".
{{ file "/path" }} Reads a secret value from a mounted file at /path, trimmed. Always required—a missing, unreadable, oversized, or disallowed path is a hard startup error.

An example from the shipped config.toml:

[api_portal.security]
encryption_key = '{{ env "APIP_AP_SECURITY_ENCRYPTION_KEY" }}'
session_secret = '{{ env "APIP_AP_SECURITY_SESSION_SECRET" }}'

Partial substitution works too—'foo-{{ env "X" }}' resolves to "foo-bar" if X=bar.

{{ file }} path allowlist

{{ file "/path" }} only reads from /etc/api-portal or /secrets/api-portal by default. Override with the APIP_CONFIG_FILE_SOURCE_ALLOWLIST environment variable (comma-separated directories)—read directly from the process environment rather than through {{ env }}, since it gates interpolation itself.

Server

[api_portal.server]
base_url = '{{ env "APIP_AP_SERVER_BASE_URL" "https://localhost:9543" }}'
port = 9543

[api_portal.server.https]
enabled = false
cert_file = "./resources/security/client-truststore.pem"
key_file = "./resources/security/private-key.pem"
Key Default Description
server.base_url https://localhost:9543 Canonical public origin, used only to build absolute URLs embedded in generated agent prompts
server.port 9543 Single listener port
server.https.enabled false Whether the listener terminates TLS itself. Set false only when a trusted upstream (proxy/LB/ingress) terminates TLS
server.https.cert_file / key_file Required only when https.enabled = true—no self-signed fallback

Logging

[api_portal.logging]
level = "info"          # debug | info | warn | error
format = "text"         # text | json
console_only = true     # true: stdout only. false: also write rotating log files to disk

Database

[api_portal.database]
driver = "sqlite"             # sqlite | postgres | mssql
path = "./api-portal.db"       # SQLite only
host = "localhost"            # PostgreSQL / MSSQL only
port = 5432                   # PostgreSQL / MSSQL only (1433 for MSSQL)
name = "api_portal"            # PostgreSQL / MSSQL only
user = "postgres"             # PostgreSQL / MSSQL only
password = ""                 # PostgreSQL / MSSQL only
ssl_mode = "disable"          # PostgreSQL / MSSQL only: disable | verify-full
ssl_root_cert = "./resources/security/ca.pem"
max_open_conns = 50
min_open_conns = 2
pool_idle_timeout_ms = 10000
pool_connection_timeout_ms = 30000
pool_request_timeout_ms = 30000         # MSSQL only — per-query execution timeout

Pool settings are validated at startup

For postgres/mssql drivers, max_open_conns must be an integer ≥ 1, the remaining pool settings must be non-negative integers, and min_open_conns must not exceed max_open_conns. An invalid value fails startup closed with a [FATAL] message rather than silently reaching the connection pool.

Security

[api_portal.security]
encryption_key = ""     # 64-char hex — AES-256-GCM key for encrypting secrets at rest
session_secret = ""     # 64-char hex — express-session signing secret

[api_portal.security.service_api_key]
enabled = true
header_name = "x-wso2-api-key"
value = ""

encryption_key and session_secret are required—the portal fails closed at startup if either doesn't resolve to a 64-character hex string. Generate one with openssl rand -hex 32.

Authentication

[api_portal.auth]
mode = "local"          # local | idp

[api_portal.auth.claim_mappings]
organization = "org_name"   # claim carrying the org ID
roles = "roles"             # claim carrying the user's roles
groups = "groups"

[api_portal.auth.local]
platform_api_url = ""
public_key_path = ""    # path to the Platform API's RS256 public key PEM
tls_skip_verify = false

[api_portal.auth.idp]
name = "IS"
issuer = "https://localhost:9443/oauth2/token"
authorization_url = "https://localhost:9443/oauth2/authorize"
token_url = "https://localhost:9443/oauth2/token"
user_info_url = "https://localhost:9443/oauth2/userinfo"
client_id = ""
client_secret = ""
audience = ""
callback_url = "http://localhost:9543/default/callback"
scope = "openid profile email"
sign_up_url = ""
logout_url = "https://localhost:9443/oidc/logout"
logout_redirect_uri = "http://localhost:9543/default"
certificate = ""
jwks_url = "https://localhost:9443/oauth2/jwks"
token_refresh_timeout_ms = 10000
silent_sso = true      # Enable silent SSO
org_callback = false   # Redirect to the org's own landing page after login

See Authentication for the authentication modes and the Asgardeo identity-provider walkthrough.

Authorization

Authorization is configured in its own section, independent of auth.mode, because both the local and IDP branches read it.

[api_portal.auth.authorization]
enabled = true
mode = "role"           # scope | role
role_to_scope_mapping = "./resources/role-to-scope-mapping.yaml"
page_role_validation = false

[api_portal.auth.authorization.portal_roles]
admin = "ap_admin"
subscriber = "ap_subscriber"

Five keys govern how a request's permissions are resolved:

Key Default Description
authorization.enabled true Master switch for Management API (/api/v0.9) authorization. With false, any authenticated caller satisfies every operation's scope list—a development opt-out that logs a startup warning
authorization.mode role How a request's effective scopes are derived. role expands the token's roles claim through the mapping table and ignores the scope claim entirely, so a caller can't widen a role's grant by asking for extra scopes. scope reads the token's own scope claim—use it when the issuer mints dp:* scopes directly. Validated even when enabled = false, so a typo surfaces immediately
authorization.role_to_scope_mapping ./resources/role-to-scope-mapping.yaml Path to the YAML grant table. Required when mode = "role". Validated at startup against the portal's OpenAPI spec whenever it's set—an undeclared dp:* scope fails startup rather than surfacing later as a role that logs in and is denied every request
authorization.page_role_validation false Per-page role-tier gating. Separate from enabled, which governs REST scopes—one switch for both would mean turning page gating off also silently disabled REST enforcement
authorization.portal_roles.admin / .subscriber ap_admin / ap_subscriber The role names, as they appear in the roles claim, that grant each page-access tier. Point them at your IDP's role names, or at names in the mapping table to drive page gating and REST authorization from the same roles

Two retired keys abort startup

Leaving either of these in config.toml fails startup by design—an ignored key would silently apply the default instead of what the file says.

Retired key Replacement
auth.role_validation auth.authorization.page_role_validation
auth.idp.roles auth.authorization.portal_roles

Note that role_validation maps to page_role_validation, not to authorization.enabled. There was also a third role tier, super_admin; it gated pages this portal doesn't serve, so it was removed.

Page access rules

Additions only—the portal always protects its own pages (applications, API keys, subscriptions, settings) regardless of what's listed here. Use this to require login/authorization for a custom page you've added:

# [api_portal.page_access_rules]
# authenticated = ["**/my-custom-page"]
# authorized = ["**/my-custom-page"]

Patterns are glob-matched (minimatch) against the request URL and merged with—never replace—the built-in list.

Organization

[api_portal.organization]
handle = "default"                       # URL slug: /{handle}/views/{viewName}
display_name = "Default"                 # Used only when first seeding the organization
auto_create_subscription_plans = true    # Auto-create Bronze/Silver/Gold/Unlimited/AsyncUnlimited

These three keys describe the organization the instance serves:

Key Description
organization.handle The URL slug of the single organization this instance serves, and the pin every route is scoped against. Anything resolving to a different organization is rejected. In local-auth mode it must match the Platform API's organization id. In IDP mode the token's organization claim is matched against the organization's idpRefId instead—see the note below
organization.display_name Used only when seeding the organization for the first time. Never overwrites an existing name, so an admin's later edit in the settings UI survives restarts. Empty means "use the handle"
organization.auto_create_subscription_plans Seeds Bronze, Silver, Gold, Unlimited, and AsyncUnlimited alongside the organization

Seeding runs on startup only if the organization doesn't already exist, so it's idempotent and safe to leave enabled.

Which token claim carries the organization

The two authentication modes resolve it differently, so don't assume one claim covers both.

  • Local auth reads a fixed org_handle claim and compares it to organization.handle.
  • IDP mode reads the claim named by auth.claim_mappings.organization (default org_name) and compares it to the organization's idpRefId, which admins set in Organization settings.

Note

organization.default_name is a deprecated alias for handle. It still resolves, with a startup warning—use handle in new configuration.

Artifacts

[api_portal.artifacts]
enabled_types = ["apis", "mcp-servers", "api-workflows"]

An allowlist of the artifact types this portal serves. A type left out gets no navigation entry, no landing-page section, and 404s on its routes. Valid entries are apis, mcp-servers, and api-workflows; an unrecognized entry aborts startup so a typo can't silently drop a type. Omit the section to serve all three. See Artifact types.

Uploads

Limits applied to every upload and to archive extraction—theme ZIPs, API specs, documents, and landing-page content.

[api_portal.uploads]
max_bytes = 10485760       # 10 MiB — a single upload, or a single entry inside an archive
max_total_bytes = 52428800 # 50 MiB — total extracted size per archive
max_zip_entries = 500
max_depth = 10

These are the ceilings the Theming panel's "up to 10 MB" hint and the Manage APIs spec upload both derive from. max_total_bytes, max_zip_entries, and max_depth guard archive extraction against a decompression bomb, so raise them only deliberately.

Note

This section isn't in config-template.toml—the values come from the built-in defaults. Add the table to config.toml to override them.

Try-out proxy

The try-it console calls an API's registered endpoint, which is a different origin from the portal. Rather than requiring every gateway to return CORS headers naming the portal, the panel can be pointed at a same-origin proxy that makes the call server-side.

[api_portal.tryout]
enabled = true
allow_http_endpoints = true    # false: only https:// endpoints may be called
allow_private_endpoints = false
tls_skip_verify = false        # development only
timeout_ms = 15000
max_request_bytes = 1048576    # 1 MiB
max_response_bytes = 5242880   # 5 MiB

The proxy's behavior and its safety limits are set by these keys:

Key Default Description
tryout.enabled true Whether the proxy is available
tryout.allow_http_endpoints true Whether cleartext http:// endpoints may be called. Intended for local development; set it to false in production so only https:// endpoints are reachable
tryout.allow_private_endpoints false Deny-by-default. The registered-endpoint allowlist can't protect against an endpoint registered to point at an internal service, so this denylist is the only control for that case. Set true when the gateway legitimately sits on a private address—a Docker Compose service name, a cluster IP, localhost—after confirming only intended services are reachable from the portal
tryout.tls_skip_verify false Development only
tryout.timeout_ms 15000 Per-request timeout
tryout.max_request_bytes 1048576 Request body ceiling. Exceeding it returns 413
tryout.max_response_bytes 5242880 Response body ceiling

Two limits hold regardless of these settings: the proxy only calls URLs contained by one of the endpoints registered for that API, so a caller can't choose an arbitrary host; and link-local and cloud-metadata addresses such as 169.254.169.254 are refused at connection time.

Design mode

# [api_portal.design_mode]
# enabled = false
# path_to_layout = "./src/defaultContent/"
# api_samples_path = "./samples/apis/"
# mcp_samples_path = "./samples/mcps/"
# subscription_plans_path = "./samples/subscription-plans.yaml"
# applications_path = "./samples/applications.yaml"

Disabled by default. See Design Mode for the full field reference and theme-development workflow.

Webhooks

[api_portal.webhooks.delivery]
poll_interval_ms = 2000
batch_size = 50
signature_tolerance_sec = 300

Global delivery tuning only—subscribers themselves are per-organization, managed on the Webhook Integration settings tab, not in this file. Each delivery is attempted exactly once; there's no retry or backoff.

signature_tolerance_sec is the window the portal's own signature verifier accepts. See the Webhook Event Catalog for the signing algorithm.