Traffic Policy
A Traffic Policy decides what happens to a request at the devgate edge, before it reaches your service: ask for a password, cap a noisy client, redirect, rewrite a path, add security headers — or answer the request outright. It is a YAML (or JSON) document, written the way ngrok writes theirs, so an existing ngrok policy usually runs as it is.
on_http_request:
- name: Staging password
expressions:
- "!req.url.path.startsWith('/webhooks')"
actions:
- type: basic-auth
config:
credentials:
- "team:correct-horse-battery"
on_http_response:
- actions:
- type: add-headers
config:
headers:
x-frame-options: DENY Attaching a policy
An agent endpoint takes its policy from the command that starts it:
$ devgate http 8080 --url https://<your dev domain> --traffic-policy-file policy.yaml The agent sends the file when the session starts; the edge validates it and the endpoint runs it from the first request. Change the file and restart the agent to change the policy. On a reserved endpoint the policy is stored with the endpoint and can also be edited in the dashboard; a cloud endpoint is nothing but its policy, edited in the dashboard with no downtime.
Phases, rules and actions
A policy has up to three phases, each a list of rules run in order:
on_tcp_connect— when the client connects, before any HTTP is read.on_http_request— for each request, before it is forwarded.on_http_response— for each response, before it goes back to the client.
A rule has an optional name, optional expressions and a list of
actions. When every expression is true (or there are none) the actions run in order. A
terminating action — custom-response, deny, redirect, close-connection, forward-internal — answers the request, and
nothing after it runs. An expression that fails to evaluate counts as false.
Expressions
Expressions are CEL, the Common Expression Language: comparisons,
&&, ||, in, string methods
(startsWith, endsWith, contains,
matches, lower, upper), plus these helpers:
getReqHeader('name')/hasReqHeader('name')— request header values (a list).getResHeader('name')/hasResHeader('name')— response header values.getQueryParam('name')/hasQueryParam('name')— query parameter values.inCidrRange(ip, '10.0.0.0/8')/inCidrRanges(ip, [...])— IP range checks.conn.client_ip.isPrivateIp()— loopback, private and link-local addresses.
Variables
| Variable | Value |
|---|---|
req.method | GET, POST… |
req.url | The full URL: https://host/path?query |
req.url.path, req.url.query, req.url.query_params | Path, raw query, and query parameters as a map of lists. |
req.url.host, req.host | The endpoint hostname; the Host header as sent. |
req.headers | Request headers, lowercase names, values as lists. |
req.user_agent, req.content_type, req.content_length, req.version | The usual request facts. |
conn.client_ip, conn.client_port | Where the visitor connects from. |
conn.ts.start, time.now | Timestamps. |
endpoint.url | This endpoint’s URL. |
res.status_code, res.headers, res.content_type | The upstream answer, in on_http_response. |
vars.* | Whatever set-vars stored. |
actions.ngrok.* | Results of earlier actions (basic-auth, rate-limit, jwt-validation, http-request, log…), under ngrok’s names; each action’s page lists its own. |
Any string in an action’s config may interpolate an expression with ${…}, e.g.
x-client: ${conn.client_ip}.
Actions
| Action | What it does | Units |
|---|---|---|
| add-headers | Adds headers to the request on its way to your service, or to the response on its way back. | free |
| remove-headers | Removes headers from the request before your service sees it, or from the response before the visitor does. | 1 |
| compress-response | Compresses your service’s answer with gzip, Brotli or deflate — whichever the visitor’s browser prefers — as it streams. | 10 |
| basic-auth | Asks visitors for a username and password before the request reaches your service. | free |
| oauth | Makes visitors sign in with Google or GitHub before they reach your service, and tells later rules who they are. | free |
| openid-connect | Makes visitors sign in with any OpenID Connect provider — Okta, Auth0, Microsoft Entra ID, Keycloak and others — before they reach your service. | free |
| jwt-validation | Lets a request through only with a valid JWT — signed by your issuer, for your audience, not expired. Everything else gets 401. | 10 |
| custom-response | Answers the request itself with a fixed status, headers and body — no upstream needed. | free |
| deny | Rejects the request with a status code, or drops the connection when it runs in on_tcp_connect. | free |
| redirect | Sends the visitor to another URL with an HTTP redirect. | 10 |
| url-rewrite | Changes the path and query your service receives, without the visitor seeing a redirect. | 10 |
| restrict-ips | Allows or denies clients by IP address or CIDR range. | 10 |
| rate-limit | Caps how many requests a client — or any bucket you define — can make in a time window. | 1 |
| circuit-breaker | Stops sending requests to a failing upstream for a while, answering 503 instead, then lets traffic back once it recovers. | 10 |
| verify-webhook | Checks a webhook’s signature at the edge, so forged events never reach your service. | 1 |
| http-request | Calls another URL from the policy — an auth service, a feature flag, an internal endpoint — so later rules can act on the answer. | 1 |
| set-vars | Stores values under vars.* for later rules’ expressions and interpolations. | free |
| log | Records metadata about a request or connection, shown with that request in the Traffic Inspector. | free |
| close-connection | Drops the client connection without sending any response. | free |
| forward-internal | Hands the request to one of your internal endpoints (*.internal), which runs its own policy and forwards to its agent. | free |
Traffic Policy units
As on ngrok, actions are priced by the work they do: most are free, and the ones that cost something are counted per request only when they actually run. remove-headers, rate-limit, verify-webhook and http-request cost 1 unit, compress-response, jwt-validation, redirect, url-rewrite, restrict-ips and circuit-breaker 10. Units are billed at $0.10 per 100,000 against your plan’s usage credit, and need a paid plan — on Free, a policy with them is refused with “Upgrade required”.
Questions people actually ask
Yes for the parts devgate runs: the same YAML or JSON shape, the same phases, CEL expressions, variable names (req.*, conn.*, res.*, vars, actions.ngrok.*), ${…} interpolation and action names. A policy that only uses the actions listed here runs unchanged.
It is checked before anything starts: the agent refuses to start with a list of errors and their paths (on_http_request[0].actions[1].config…), and the dashboard shows them as you type.
Expressions and interpolations are compiled once, when the endpoint’s policy is loaded; per request the edge only evaluates them, which takes microseconds.
add-headers, basic-auth, oauth, openid-connect, custom-response, deny, set-vars, log, close-connection and forward-internal cost nothing. remove-headers, rate-limit, verify-webhook and http-request cost 1 Traffic Policy unit per run; compress-response, jwt-validation, redirect, url-rewrite, restrict-ips and circuit-breaker cost 10. Units are $0.10 per 100,000 and need a paid plan.