Config File (rws.config.toml)
rws.config.toml is an optional TOML file placed in the server’s working
directory. It provides a convenient alternative to environment variables and CLI
flags. Any key set here overrides the built-in default and any system environment
variable, but can be overridden by a CLI flag.
The file is auto-detected at startup. Override the path with the
RWS_CONFIG_FILE environment variable:
RWS_CONFIG_FILE=/etc/rws/prod.toml rws[server]
Basic server settings. All keys map to the corresponding RWS_CONFIG_*
environment variable.
[server]ip = "0.0.0.0" # RWS_CONFIG_IP (default: 0.0.0.0)port = 7878 # RWS_CONFIG_PORT (default: 7878)thread_count = 200 # RWS_CONFIG_THREAD_COUNT (default: 200)[cors]
CORS policy applied to every response. All fields are hot-reloadable.
[cors]# Allow every origin. When true, the specific allow_* fields below are ignored.allow_all = true
# Restrict to specific origins (comma-separated). Used when allow_all = false.allow_origins = "https://app.example.com,https://admin.example.com"
# Allowed HTTP methods (comma-separated).allow_methods = "GET,POST,PUT,DELETE,OPTIONS"
# Allowed request headers, in lowercase (comma-separated).allow_headers = "content-type,authorization,x-api-key"
# Whether to allow cookies and credentials with cross-origin requests.allow_credentials = "true"
# Response headers the browser may expose to JavaScript (comma-separated, lowercase).expose_headers = "x-request-id,x-rate-limit-remaining"
# Preflight cache duration in seconds (default: 86400 = 24 hours).max_age = 86400[rate_limit]
Global rate-limit policy applied per client IP. Hot-reloadable.
[rate_limit]max_requests = 1000 # Requests allowed per window (default: 1000; 0 disables)window_secs = 60 # Sliding window length in seconds (default: 60)[[virtual_host]]
Repeat this section for each domain that needs its own TLS certificate. The SNI resolver selects the right certificate automatically at handshake time.
[[virtual_host]]domain = "example.com"cert_file = "/etc/ssl/example.com.pem"key_file = "/etc/ssl/example.com.key"
[[virtual_host]]domain = "api.example.com"cert_file = "/etc/ssl/api.example.com.pem"key_file = "/etc/ssl/api.example.com.key"[[upstream]]
Defines a named backend pool. Upstreams are referenced by name from routes.
[[upstream]]name = "api"backends = ["localhost:3000", "localhost:3001"]strategy = "round_robin" # "round_robin" | "random" | "ip_hash" (default: round_robin)
[upstream.health_check]path = "/health" # HTTP path polled on each backend (default: /health)interval_secs = 30 # Poll interval (default: 30)timeout_ms = 5000 # Connect + read timeout per poll (default: 5000)healthy_threshold = 2 # Consecutive passes before marking healthy (default: 2)unhealthy_threshold = 3 # Consecutive failures before marking unhealthy (default: 3)When the health checker removes all backends, the route returns 502 Bad Gateway
until at least one backend recovers.
[[route]]
Each [[route]] entry maps a set of match criteria to an action and an optional
per-route middleware stack. Routes are evaluated in declaration order; the first
match wins.
[route.match]
All criteria are optional. A request must satisfy every criterion that is set.
[[route]]name = "api-proxy"
[route.match]host = "api.example.com" # SNI hostname or Host header (exact match)path = "/api/*" # Prefix match when ending with *; exact match otherwisemethod = "POST" # HTTP method (case-insensitive; matches any if omitted)content_type = "application/json" # Content-Type prefix match[route.action] — proxy
Forward requests to a named upstream pool.
[route.action]type = "proxy"
[route.action.proxy]upstream = "api" # Must match a [[upstream]] nameconnect_timeout_ms = 5000 # (default: 5000)read_timeout_ms = 30000 # (default: 30000)strip_path_prefix = "/api" # Strip this prefix before forwarding (optional)add_path_prefix = "/v2" # Add this prefix after stripping (optional)[route.action] — grpc
Forward gRPC traffic over HTTP/2 to a named upstream.
[route.action]type = "grpc"
[route.action.grpc]upstream = "grpc-backend"connect_timeout_ms = 5000read_timeout_ms = 30000[route.action] — static
Serve static files from a directory on disk.
[route.action]type = "static"
[route.action.static]root = "./public"index = ["index.html", "index.htm"][route.action] — redirect
Issue an HTTP redirect.
[route.action]type = "redirect"
[route.action.redirect]location = "https://new.example.com$path" # $path is replaced with the request URIstatus = 301 # (default: 301)[route.action] — respond
Return a fixed response body without proxying.
[route.action]type = "respond"
[route.action.respond]status = 200body = "{\"ok\":true}"content_type = "application/json" # (default: text/plain)[route.action] — mcp
Mount the MCP Streamable HTTP server on this route.
[route.action]type = "mcp"[route.middleware]
Per-route middleware applied before the action. All sub-sections are optional.
[route.middleware.rate_limit]max_requests = 100window_secs = 60
[route.middleware.cache]ttl_secs = 300vary_by = ["Accept-Encoding", "Authorization"]
[route.middleware.auth]type = "bearer" # "bearer" | "jwt" | "basic"token_env = "API_TOKEN" # env var name holding the expected token (bearer)# secret_env = "JWT_SECRET" # env var name holding the JWT secret (jwt)# users_file = "users.htpasswd" # path to htpasswd file (basic)
[route.middleware.ip_filter]allow = ["10.0.0.0/8", "192.168.1.0/24"]deny = ["10.0.0.5"]Request rewrite rules
[[route.middleware.rewrite.request]]type = "set_header"name = "X-Forwarded-Proto"value = "https"
[[route.middleware.rewrite.request]]type = "strip_path_prefix"prefix = "/api"
[[route.middleware.rewrite.request]]type = "add_path_prefix"prefix = "/v2"
[[route.middleware.rewrite.request]]type = "remove_header"name = "X-Internal-Secret"
[[route.middleware.rewrite.request]]type = "set_uri"value = "/health"Response rewrite rules
[[route.middleware.rewrite.response]]type = "set_header"name = "X-Powered-By"value = "rws"
[[route.middleware.rewrite.response]]type = "remove_header"name = "Server"
[[route.middleware.rewrite.response]]type = "set_status"code = 200reason = "OK"
[[route.middleware.rewrite.response]]type = "replace_body"from = "old-domain.com"to = "new-domain.com"[[tcp_proxy]]
Standalone L4 TCP proxy. Binds its own port and relays bytes bidirectionally between the client and a backend. Runs in a background thread and is independent of the HTTP server.
[[tcp_proxy]]name = "db-proxy"listen = "0.0.0.0:5433" # Address to bindbackends = ["db-primary:5432", "db-replica:5432"]connect_timeout_ms = 5000 # (default: 5000)[[udp_proxy]]
Standalone UDP datagram proxy. Each datagram is forwarded to a backend and the reply is relayed back to the originating client.
[[udp_proxy]]name = "dns-proxy"listen = "0.0.0.0:5353"backends = ["8.8.8.8:53", "8.8.4.4:53"]reply_timeout_ms = 5000 # How long to wait for a reply (default: 5000)buffer_size = 65536 # Datagram buffer in bytes (default: 65536)[[ws_proxy]]
Standalone WebSocket proxy. Handles the HTTP upgrade handshake then relays raw bytes bidirectionally between client and backend.
[[ws_proxy]]name = "ws-backend"listen = "0.0.0.0:8080"backends = ["localhost:8081"]connect_timeout_ms = 5000 # (default: 5000)read_timeout_ms = 30000 # (default: 30000)Complete example
[server]ip = "0.0.0.0"port = 7878thread_count = 200
[cors]allow_all = falseallow_origins = "https://app.example.com"allow_methods = "GET,POST,PUT,DELETE"allow_headers = "content-type,authorization"max_age = 86400
[rate_limit]max_requests = 500window_secs = 60
[[virtual_host]]domain = "example.com"cert_file = "/etc/ssl/certs/example.pem"key_file = "/etc/ssl/private/example.key"
[[upstream]]name = "api"backends = ["localhost:3000", "localhost:3001"]strategy = "round_robin"
[upstream.health_check]path = "/health"interval_secs = 15healthy_threshold = 2unhealthy_threshold = 3
[[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 = 100window_secs = 60
[[route]]name = "health"
[route.match]path = "/ping"
[route.action]type = "respond"
[route.action.respond]status = 200body = "pong"