Routing¶
Routes are evaluated in declaration order — the first match wins.
{
match: {
domain: "*.example.com", // optional, segment glob
path: "/api/*", // optional if domain set
methods: ["GET", "POST"], // optional, empty = all
},
set: { port: "8080" }, // optional, route-level variables for templates
speed: { download_mbps: 50 }, // optional, bandwidth throttling
action: "proxy_to_backend", // string ref to actions map
}
Route-level variables defined in set are available as {key} placeholders in the action's upstream template. This allows multiple routes to share a single action with different parameters.
At least one of domain or path must be specified. Omit match entirely for a catch-all route:
Domain Matching¶
Domain patterns use segment-based glob matching:
*matches exactly one domain label (like wildcard SSL certificates)cdn-*,*-prod— partial wildcards match a label with a fixed prefix or suffix**matches one or more domain labels (only valid as the last segment)
| Pattern | Matches | Does not match |
|---|---|---|
example.com |
example.com |
sub.example.com |
*.example.com |
sub.example.com |
example.com, a.b.example.com |
*.test.example.com |
api.test.example.com |
test.example.com |
test.*.example.com |
test.staging.example.com |
test.example.com |
*.*.example.com |
a.b.example.com |
a.example.com, a.b.c.example.com |
cdn-*.example.com |
cdn-us.example.com, cdn-eu.example.com |
cdn.example.com, web-us.example.com |
*-prod.example.com |
api-prod.example.com |
api-staging.example.com |
*.storage.** |
cdn.storage.example.com, cdn.storage.a.b.c |
storage.example.com, cdn.storage |
cdn-*.** |
cdn-us.example.com, cdn-eu.myapp.dev |
cdn.example.com |
Domain matching is case-insensitive. Ports are stripped automatically (example.com:443 → example.com).
Domain patterns are also used for L4 dispatching — the SNI hostname from the TLS ClientHello is matched against the same patterns.
// Virtual hosting — one listener, multiple domains.
{
services: {
gateway: {
listen: ":443",
tls: true,
tls_cert: "cert.pem",
tls_key: "key.pem",
routes: [
{ match: { domain: "api.example.com", path: "/v1/*" }, action: "api" },
{ match: { domain: "*.cdn.example.com" }, action: "cdn" },
{ match: { domain: "*.example.com", path: "/*" }, action: "site" },
],
},
},
}
Forward Proxy¶
The forward_proxy matcher selects routes that handle forward proxy requests — HTTP requests with an absolute URL in the request line (e.g. GET http://example.com/path).
When a browser extension or system proxy setting sends traffic through prox, HTTPS sites use CONNECT tunneling while plain HTTP sites send standard requests with absolute URLs. The forward_proxy: true matcher catches the latter case.
Key behavior:
forward_proxy: true— matches only forward proxy requests (absolute URL). Regular reverse proxy requests are ignored.- Without
forward_proxy(default) — forward proxy requests are rejected by the route. This prevents accidental matching ofGET http://evil.comagainst a catch-allpath: "/*"reverse proxy route. pathanddomainmatchers are ignored for forward proxy routes (they belong to the target site, not the proxy server).methodscan be combined withforward_proxyto filter by HTTP method.
Example: combined CONNECT + HTTP forward proxy:
// HTTPS traffic — CONNECT tunneling
{
match: { domain: "*.**", methods: ["CONNECT"] },
action: { type: "proxy", upstream: "squid:3128" },
},
// HTTP traffic — forward proxy with absolute URL
{
match: { forward_proxy: true },
action: { type: "proxy", upstream: "squid:3128" },
},
Multiple Path Patterns¶
A single route can match multiple paths by listing them as a comma-separated string:
{
match: { domain: "app.example.com", path: "/assets/*, /api/*, /ws" },
action: { type: "proxy", upstream: "http://backend:3000" },
}
The request matches if any of the listed patterns match (OR logic). Each pattern follows the same rules — exact match or wildcard prefix (/api/*). Whitespace around commas is trimmed.
Inline Actions¶
Instead of referencing a named action, an action definition may be inlined directly:
{
match: { path: "/health" },
action: {
type: "static",
status: 200,
body_ref: { text: "OK" }, // inline resource
},
}
Route Includes¶
Routes can be loaded from external files. A string path in the routes array references an external file whose routes are spliced in place, preserving order.
{
listen: ":443",
tls: true,
tls_cert: "./certs/",
routes: [
"./routes/realtime.json5", // routes from file (spliced in order)
"./routes/fallback.json5", // another include
],
}
Route include files support two formats:
Bare array — routes only:
// routes/realtime.json5
[
{
match: { domain: "*.**", path: "/ws" },
action: { type: "proxy", upstream: "localhost:3505" },
},
{
match: { domain: "*.**", path: "/grpc" },
action: { type: "proxy", upstream: "localhost:3506" },
},
]
Object wrapper — routes inside a routes key:
// routes/realtime.json5
{
routes: [
{
match: { domain: "*.**", path: "/ws" },
action: { type: "proxy", upstream: "localhost:3505" },
},
],
}
Inline routes and includes may be mixed freely:
{
listen: ":443",
routes: [
"./routes/realtime.json5",
{ match: { path: "/health" }, action: { type: "static", status: 200 } },
"./routes/fallback.json5",
],
}
Relative paths are resolved from the directory of the parent config file. Included files are tracked by the file watcher — editing a route include triggers a hot reload. Circular references are detected and rejected.