API Exposure Guide

API Proxying & Configuration

Securely proxy and reshape vendor APIs, and configure graph-driven dynamic routes.

API Proxying & Configuration (proxy.toml)

Application Modules in Rescile (data/app/) can host custom front-ends. To securely communicate with external APIs (or bridge declared state and live state), define backend proxy rules in app/proxy.toml.

Note: Proxy routes are automatically evaluated in descending order of their path length. This ensures that more specific, longer paths will always match before shorter, more generic ones.

CE security note: rescile-ce does not authenticate requests to /apps/<module> proxy routes. Any action:// tunnel or external API exposure declared in a proxy rule is reachable to anyone who can reach the controller. Use rescile-ee with configured authentication for proxies that reach credentials or sensitive APIs.

Secure Static Proxying

Secrets can be securely injected using Tera templating or regex expansion, hiding them from the browser. For more on secrets in proxy configurations, see Security & Secrets and the proxy.toml Reference.

[[route]]
path = "/api/billing"
target = "http://internal-billing-service:8080/v1"
forward_headers = ["Content-Type", "Accept"]
inject_headers = { "Authorization" = "Bearer ${env.BILLING_API_TOKEN}" }

Graph-Driven Dynamic Routing

By defining an origin_resource, the proxy engine iterates over all nodes of that type in the graph. It evaluates the match_on rules for each node. For every match, it dynamically mounts a new, distinct proxy route.

[[route]]
origin_resource = "firewall"
match_on = [{ property = "managed", value = "true" }]
# Generates a unique path for every firewall in the graph
path = "/api/state/firewalls/{{ origin_resource.name }}/rules"
# Routes to the specific firewall's management IP
target = "{{ origin_resource.api_endpoint }}/api/v1/rules"
inject_headers = { "X-Auth-Token" = "${env.FIREWALL_TOKEN_{{ origin_resource.name | upper }}}" }

Modifying the Inbound Request (request_filter)

When the front-facing client contract differs from the backend API’s expected payload, use request_filter to reshape incoming request bodies before they are forwarded upstream:

[[route]]
path = "/api/v1/storage/volumes"
target = "https://backend.internal/v2/volumes"

[route.request_filter]
"tera!" = '''
{
  "vol_name": "{{ body.name }}",
  "bytes": {{ body.size_gb * 1073741824 }}
}
'''

Modifying the API Response (response_filter)

Often, upstream APIs return bloated or deeply nested JSON that doesn’t fit the schema your frontend expects. The response_filter directive intercepts the upstream response and transforms it before returning it to the client.

You can reshape the response using JMESPath or Tera:

Using JMESPath (jp!): Ideal for fast, structural JSON filtering.

[[route]]
path = "/api/upstream/nodes"
target = "http://internal-api/v2/nodes"

[route.response_filter]
# Extracts just the names of nodes that have status == 'active'
"jp!" = "data.nodes[?status == 'active'].name"

Using Tera (tera!): Ideal for complete schema reshaping, combining secrets, or injecting contextual graph metadata. The upstream JSON is accessible via the response variable, and the full in-memory architecture graph is available via the graph variable.

[[route]]
path = "/api/state/vms"
target = "https://vcenter.internal/api/vms"

# Reshape the deeply nested vCenter JSON response into the standard enterprise contract
[route.response_filter]
"tera!" = '''
{
  "items": [
    {% for vm in response.value %}
    { "name": "{{ vm.name }}", "status": "{{ vm.power_state }}" }{% if not loop.last %},{% endif %}
    {% endfor %}
  ],
  "source": "vCenter"
}
'''

Normalizing Response Status & Headers (response_mapping)

Backends often return non-standard status codes (e.g., 200 OK containing an error envelope). The response_mapping block ensures client contract conformance:

[[route]]
path = "/api/v1/deploy"
target = "http://upstream-service/deploy"

[route.response_mapping]
status_from_body = [
  { when = "response.status == 'FAILED'", return_status = 400 },
  { when = "response.status == 'NOT_FOUND'", return_status = 404 }
]
override_headers = { "X-API-Contract" = "v1" }

Action-owned service routes

Use action://<module>/<action>/<path> to route an application request to one running service action. Define the action with run_mode = "service", an exec command that starts the HTTP server, and [runtime.expose] for its TCP or Unix-socket listener. Set auto_execute = true if it should start automatically. The action may choose its own upstream backends; a request cannot choose an arbitrary runner-side destination.

# app/proxy.toml
[[route]]
path = "/backend/*"
target = "action://my_module/my_long_running_service/backend"

On a connected, tunnel-capable runner, the controller forwards requests to the action-owned listener over the runner’s existing outbound gRPC connection. No inbound connection to the runner is needed. action:// routes need no separate service registration or route-level opt-in. Older runners can still use their directly reachable endpoint where supported; a failed tunnel request does not retry via direct HTTP. A missing or ambiguous running action returns 503.

The controller rewrites same-origin redirects, HTML/CSS asset URLs, and backend cookies under /apps/<module>/backend/. It forwards Accept and Content-Type and any other allowed headers named in forward_headers. Browser Authorization and Rescile authentication cookies are not forwarded; only cookies issued for this mount are restored for the service. Do not use inject_headers, request_filter, response_filter, or response_mapping on tunneled routes: these currently return 501, not a partially filtered response.

Tunnel limits: 1 MiB per request/response body, 16 KiB and 64 fields per header direction, 32 concurrent requests per runner, 30-second controller deadline, and 25-second backend timeout. Supported methods: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS. WebSockets, SSE, large uploads, and standalone portal tunneling are not supported. Client-side JavaScript-generated absolute backend URLs are not rewritten; use relative URLs or configure the backend’s public base path.

Runners advertise tunnel capabilities. The binary-header capability carries allowed HTTP header values as bytes, including values that are not UTF-8. During staggered deployment, older peers use text headers and may omit values they cannot convert. In binary mode, malformed cookies and headers needed for URL rewriting fail rather than bypassing mount scoping. Browsers may still reject nonstandard cookie bytes. Deploy runner and controller versions independently, but keep both protobuf contracts aligned.

See Action examples for [runtime.expose] and Agent Runners & Services for deployment and authentication. CE has no built-in authentication for app requests or runner gRPC: isolate its runner endpoint. EE accepts runner gRPC tokens from runner-* API keys and applies configured app authentication. Use TLS for remote runner tokens.