Routing, rate-limits, quotas, redaction, and access control all speak the same language: CEL. Policies compile and type-check at config-load, not at first request. Invalid policies fail loud on boot.
| Variable | Type | Notes |
|---|---|---|
request | Request | model, messages, headers, backend, path, size, stream |
tenant | Tenant | id, plan, region, flags |
user | User | id, email, role (from auth header) |
quota | Quota | remaining, limit, resets_at |
receipt | Receipt | id, tokens_in, tokens_out, cost, annotations |
time | Timestamp | current time (seconds since epoch) |
allow(), deny(reason): pre-request disposition.annotate(key, value): attach metadata to the receipt.route(upstream_id): pick the upstream for this request.rate_limit(key, per_minute, per_hour): enforce a rate cap, keyed arbitrarily.quota_spend(amount): deduct from the quota ledger.redact_field(path, pattern): mutate a specific field.challenge_402(price, currency): issue an x402 payment challenge.Standard CEL rules apply: short-circuit && and ||, ternary ?:, membership with in, string methods like startsWith and contains. No loops. No I/O. Expressions are pure; side effects come from the built-in functions above.
When you run relaygate config apply, every policy is compiled and type-checked. Type errors fail the load. The error message points at the file, line, and token:
error: type mismatch in policy 'route-by-plan'
/etc/relaygate/policies/route.cel:4:18
tenant.plan == 'pro' && request.modl == 'gpt-5'
^^^^
no such field 'modl' on type Request
did you mean 'model'?
tenant.plan == "free" && request.model == "gpt-5"
? deny("gpt-5 is not on the free plan")
: allow()
tenant.plan == "pro" ? route("anthropic-direct") : route("openai")
rate_limit(
key = "tenant:" + tenant.id,
per_minute = tenant.plan == "pro" ? 1200 : 60
)
quota.remaining <= 0
? deny("quota exhausted; resets at " + string(quota.resets_at))
: quota_spend(1)
request.messages.size() > 0
&& request.messages[request.messages.size() - 1].content.contains("code:")
? route("anthropic-direct")
: route("openai")
// between 22:00 and 06:00 UTC, route to the cheaper backend
time.getHours() >= 22 || time.getHours() < 6
? route("openrouter")
: route("openai")
user.role != "admin"
? redact_field("request.messages[*].content", "\\b\\d{3}-\\d{2}-\\d{4}\\b")
: allow()
request.path.startsWith("/v1/agents/")
&& !request.headers.exists("x-truecom-settlement")
? challenge_402(0.004, "USDC")
: allow()
Next: Observability →