proxy.toml Reference
Placed in app/proxy.toml to configure API routing for Application Modules.
[[route]]
# Path pattern with dynamic parameter capture
path = "/api/v1/pools/{pool_id}/volumes/{vol_id}"
target = "https://backend.internal/v2/pools/{{ request.params.pool_id }}/vols/{{ request.params.vol_id }}"
# Forward these headers from the client
forward_headers = ["Accept", "Authorization"]
# Inject secure headers from the backend
[route.inject_headers]
"Authorization" = "Bearer ${env.API_TOKEN}"
# Inbound payload transformation (client contract -> backend payload)
[route.request_filter]
"tera!" = '{"volume_name": "{{ request.params.vol_id }}", "size_bytes": {{ body.size_gb * 1024 }}}'
# Outbound payload transformation (backend response -> client contract)
[route.response_filter]
"jmespath!" = '{"id": id, "status": state}'
# Status code and header normalization
[route.response_mapping]
status_from_body = [
{ when = "response.status == 'failed'", return_status = 400 }
]
override_headers = { "Content-Type" = "application/json" }
# Graph-driven dynamic routes
[[route]]
origin_resource = "provider"
match_on = [{ property = "type", value = "internal" }]
path = "/api/{{ origin_resource.name }}/*"
target = "{{ origin_resource.api_url }}/"
Variables like ${env.X} or ${vault:collection#secret} use fast regex expansion. Variables like {{ env.X }} or {{ vault(collection="col", secret="sec") }} use full Tera templating and have access to the request context object (for HMAC signing).
Vault Secrets Injection
You can inject secrets directly from Rescile Vault into URLs or headers using either the fast ${vault:collection#secret} regex syntax or the {{ vault(collection="...", secret="...") }} Tera function:
[route.inject_headers]
"Authorization" = "Bearer ${vault:my-collection#my-api-token}"
"X-Service-Key" = "{{ vault(collection='services', secret='key') }}"
Secrets are resolved and decrypted lazily at request time. If a secret or collection cannot be found or decrypted, the request fails fast with HTTP 502 Bad Gateway without leaking details to the client.
Path Patterns & Dynamic Parameters
Routes support path templates with curly brackets {param_name}, e.g. /api/v1/pools/{pool_id}/volumes/{vol_id}:
- Captured path variables are extracted automatically into
request.params.<key>. request.paramscan be referenced intargetURLs,inject_headers,request_filter, andresponse_filter.
Inbound Request Transformation (request_filter)
The request_filter intercepts incoming client bodies before dispatching upstream:
"tera!": Renders a new payload string. Receivesbody(parsed JSON if valid, else raw string),request.params,request.method, graph context, and secrets."jmespath!"/"jp!": Executes a JMESPath transformation against the inbound JSON body.
Response Status & Header Mapping (response_mapping)
Normalizes backend status codes and headers to enforce a consistent client contract:
status_from_body: Evaluates conditional expressions (when) using Tera against the final response JSON. If condition renderstrue, overrides the HTTP response status withreturn_status.override_headers: Injects or replaces headers returned to the client.
Context Variables
The following context is available during Tera templating (for target, headers, filters, and responses):
request.method: HTTP method (e.g.,GET,POST).request.path: The URI path of the request.request.params: Key-value map of path parameters captured from{param}route placeholders.request.host: TheHostheader orX-Forwarded-Host.request.scheme: The HTTP scheme (httporhttps).request.base_url: Combination of scheme and host.request.query: Object containing all query string parameters.request.query_values: Concatenated values of sorted query arguments (useful for signing).request.headers: Object containing incoming request headers.request.body: The raw text of the incoming HTTP body.request.timestamp: Epoch time in seconds.origin_resource: Properties of the currently matched graph node (iforigin_resourceis defined).
Dynamic Request Signatures:
For APIs that require every request to be cryptographically signed (such as Exoscale’s v2 API)
# app/proxy.toml
[[route]]
path = "/api/exoscale"
target = "https://api-ch-dk-2.exoscale.com/v2"
forward_headers = ["Accept", "User-Agent"]
[route.inject_headers]
"Content-Type" = "application/json"
"Authorization" = """
{%- set expires = request.timestamp + 600 -%}
{%- set msg = request.method ~ " " ~ request.path ~ "\n" ~ request.body ~ "\n" ~ request.query_values ~ "\n\n" ~ expires -%}
{%- set sig = msg | hmac_sha256(key=env.EXOSCALE_API_SECRET, encoding="base64") -%}
EXO2-HMAC-SHA256 credential={{ env.EXOSCALE_API_KEY }}{% if request.signed_query_args %},signed-query-args={{ request.signed_query_args }}{% endif %},expires={{ expires }},signature={{ sig -}}
"""
Action Routing
Route to a running service action with action://<module_id>/<action_name>/<path>. The service action must launch its HTTP server with exec and declare its listener with [runtime.expose]. No extra proxy target type or service registry is needed.
[[route]]
path = "/acme/*"
target = "action://cert-manager/start_acme_gateway/acme"
forward_headers = ["X-Request-ID"] # Optional; Accept and Content-Type are forwarded by default
On a tunnel-capable connected runner, the controller selects that action’s listener and forwards HTTP over its outbound gRPC stream; older runners may use directly reachable endpoints. It does not launch an action on each HTTP request. Authorization, browser authentication cookies, and hop-by-hop headers are not forwarded even if listed in forward_headers. Backend cookies are scoped to the /apps/<module>/acme/ mount. Tunneled routes return 501 when configured with inject_headers, request_filter, response_filter, or response_mapping.
Body limit: 1 MiB in each direction. Header limit: 16 KiB and 64 fields in each direction. A runner advertising binary-header support can carry allowed non-UTF-8 header values; older peers remain text-only. See Action-owned service routes for methods, URL rewriting, authentication, error handling, and streaming limitations.