Config-Driven Proxy
When rws.config.toml (or the file pointed to by RWS_CONFIG_FILE) contains at least one [[route]] or [[upstream]] section, rws boots in proxy mode automatically. No code changes required.
Minimal example
[[upstream]]name = "api"backends = ["localhost:3000"]
[[route]]name = "api-proxy"
[route.match]path = "/api/*"
[route.action]type = "proxy"
[route.action.proxy]upstream = "api"[[upstream]] — backend pools
Each [[upstream]] block defines a named pool of HTTP backends.
[[upstream]]name = "api" # required; referenced by route actionsbackends = ["10.0.0.1:3000", "10.0.0.2:3000"] # one or more "host:port" stringsstrategy = "round_robin" # currently only "round_robin" is active[upstream.health_check] — automatic health monitoring
Add a nested [upstream.health_check] table to enable background health checking. Unhealthy backends are removed from the live rotation until they recover.
[[upstream]]name = "api"backends = ["10.0.0.1:3000", "10.0.0.2:3000", "10.0.0.3:3000"]
[upstream.health_check]path = "/healthz" # GET path sent to each backendinterval_secs = 30 # how often to probe (default: 30)timeout_ms = 5000 # connect + read timeout per probe (default: 5000)healthy_threshold = 2 # consecutive successes before marking live (default: 2)unhealthy_threshold = 3 # consecutive failures before marking dead (default: 3)See Health Checks for the full implementation details.
[[route]] — routing rules
Routes are evaluated in declaration order; the first match wins.
[[route]]name = "my-route" # informational label; no functional effect[route.match] — matching criteria
All fields are optional. Omitting a field means “match anything”.
[route.match]host = "api.example.com" # SNI hostname (TLS) or Host header (plain HTTP)path = "/api/*" # prefix match when ending with *, exact match otherwisemethod = "POST" # HTTP method (case-insensitive)content_type = "application/json*" # Content-Type prefix match when ending with *Path matching rules:
| Pattern | Matches |
|---|---|
/api/* | Any path starting with /api/ (prefix match) |
/api/ping | Only /api/ping (exact match) |
| (omitted) | All paths |
[route.action] — what to do on match
The type field selects the action.
type = "proxy" — forward to an upstream
[route.action]type = "proxy"
[route.action.proxy]upstream = "api" # upstream name defined in [[upstream]]connect_timeout_ms = 5000 # TCP connect timeout in ms (default: 5000)read_timeout_ms = 30000 # response read timeout in ms (default: 30000)strip_path_prefix = "/api" # strip this prefix before forwarding (optional)add_path_prefix = "/v2" # prepend this prefix before forwarding (optional)type = "grpc" — forward gRPC to an HTTP/2 upstream
[route.action]type = "grpc"
[route.action.grpc]upstream = "grpc-svc"connect_timeout_ms = 5000read_timeout_ms = 30000type = "redirect" — HTTP redirect
[route.action]type = "redirect"
[route.action.redirect]location = "https://example.com$path" # $path is replaced with the request URIstatus = 301 # 301, 302, 307, or 308 (default: 301)type = "respond" — fixed response
[route.action]type = "respond"
[route.action.respond]status = 200body = "{\"status\":\"ok\"}"content_type = "application/json"[route.middleware] — per-route middleware
Middleware is applied only to requests that match this route.
Rate limiting
[route.middleware.rate_limit]max_requests = 100 # requests allowed per window (default: 1000)window_secs = 60 # sliding window size in seconds (default: 60)Bearer token authentication
[route.middleware.auth]type = "bearer"token_env = "API_TOKEN" # environment variable holding the expected tokenIncoming requests must include Authorization: Bearer <value of API_TOKEN>. Returns 401 Unauthorized on mismatch.
IP filter
[route.middleware.ip_filter]allow = ["10.0.0.0/8", "192.168.1.100"] # allowlist (CIDR or exact)deny = ["1.2.3.4"] # denylistRequest rewriting
[[route.middleware.rewrite.request]]type = "set_header"name = "X-Real-IP"value = "client"
[[route.middleware.rewrite.request]]type = "strip_prefix"prefix = "/internal"Response rewriting
[[route.middleware.rewrite.response]]type = "set_header"name = "Cache-Control"value = "no-store"
[[route.middleware.rewrite.response]]type = "replace_body"from = "staging.internal"to = "example.com"L4 proxy sections
[[tcp_proxy]] — raw TCP tunnel
[[tcp_proxy]]name = "pg"listen = "0.0.0.0:5432"backends = ["db-1:5432", "db-2:5432"]connect_timeout_ms = 5000[[udp_proxy]] — UDP datagram proxy
[[udp_proxy]]name = "dns"listen = "0.0.0.0:53"backends = ["8.8.8.8:53", "8.8.4.4:53"]reply_timeout_ms = 2000buffer_size = 65536[[ws_proxy]] — WebSocket proxy
[[ws_proxy]]name = "chat"listen = "0.0.0.0:8080"backends = ["chat-backend:9000"]connect_timeout_ms = 5000read_timeout_ms = 30000Global middleware
Middleware that applies to all routes goes in a top-level [middleware] section (same field structure as [route.middleware]).
[middleware.rate_limit]max_requests = 500window_secs = 60Full annotated example
# rws.config.toml — full proxy setup
# ── upstreams ──────────────────────────────────────────────────────────────────
[[upstream]]name = "api"backends = ["api-1:3000", "api-2:3000"]strategy = "round_robin"
[upstream.health_check]path = "/healthz"interval_secs = 15timeout_ms = 3000healthy_threshold = 2unhealthy_threshold = 3
[[upstream]]name = "grpc-svc"backends = ["grpc-1:50051"]
# ── routes ─────────────────────────────────────────────────────────────────────
[[route]]name = "maintenance-page"
[route.match]host = "down.example.com"
[route.action]type = "respond"
[route.action.respond]status = 503body = "Under maintenance"content_type = "text/plain"
[[route]]name = "grpc"
[route.match]content_type = "application/grpc*"
[route.action]type = "grpc"
[route.action.grpc]upstream = "grpc-svc"
[[route]]name = "api"
[route.match]path = "/api/*"
[route.action]type = "proxy"
[route.action.proxy]upstream = "api"strip_path_prefix = "/api"
[route.middleware.rate_limit]max_requests = 200window_secs = 60
[route.middleware.auth]type = "bearer"token_env = "API_SECRET"
[[route]]name = "redirect-www"
[route.match]host = "example.com"
[route.action]type = "redirect"
[route.action.redirect]location = "https://www.example.com$path"status = 301
# ── L4 proxies ─────────────────────────────────────────────────────────────────
[[tcp_proxy]]name = "postgres"listen = "0.0.0.0:5432"backends = ["pg-primary:5432"]
[[udp_proxy]]name = "dns"listen = "0.0.0.0:53"backends = ["8.8.8.8:53", "8.8.4.4:53"]