Pragmatic.Gateway
YARP-based API gateway for Pragmatic.Design applications, with Agent-driven routing, maintenance mode, tenant-aware proxying, and per-cluster resilience.
Status: implemented preview subsystem. The gateway is already functional inside the repo. It is still evolving operationally, but it is no longer just a scaffold or design placeholder.
What It Does
Section titled “What It Does”- Reverse proxy based on YARP
- Dynamic routes and clusters loaded from
Pragmatic.Agent - Static route fallback from configuration
- Maintenance middleware and graceful drain behavior
- Tenant-aware request routing
- JWT authentication, CORS, response compression, and rate limiting
- Per-cluster timeout and circuit-breaker policies via
Pragmatic.Resilience
Quick Start
Section titled “Quick Start”1. Configure the gateway
Section titled “1. Configure the gateway”{ "Gateway": { "HttpUrl": "http://*:8080", "AgentSocketPath": "/var/run/pragmatic/agent.sock", "Routes": [ { "RouteId": "booking", "Path": "/booking/{**catch-all}", "Backend": "http://localhost:5010" } ] }}2. Run it
Section titled “2. Run it”dotnet run --project Pragmatic.Gateway/src/Pragmatic.Gateway/Pragmatic.Gateway.csprojUseful overrides:
dotnet run --project Pragmatic.Gateway/src/Pragmatic.Gateway/Pragmatic.Gateway.csproj -- --listen http://*:8081 --agent-socket /tmp/pragmatic/agent.sock3. Add resilience
Section titled “3. Add resilience”{ "Gateway": { "Resilience": { "Default": { "Timeout": "00:00:10", "FailureThreshold": 5, "BreakDuration": "00:00:30" }, "Clusters": { "booking": { "Timeout": "00:00:05", "FailureThreshold": 3 } } } }}Resilience is per-cluster: Default applies to every backend, and Clusters overrides
individual clusters by YARP cluster ID. Each policy maps to ClusterResiliencePolicy
(Timeout, CircuitBreakerEnabled, FailureThreshold, BreakDuration,
FailureStatusCodeMin/Max).
Circuit-breaker behavior
Section titled “Circuit-breaker behavior”ProxyResilienceMiddleware runs inside the YARP proxy pipeline and maintains a circuit per
cluster (state in ICircuitBreakerStateStore, in-memory by default):
- Closed — requests flow through. A response whose status falls in the failure range
(default
500–599), a timeout, or a backend connection error is counted as a failure. Any non-failure response resets the failure count. Once consecutive failures reachFailureThreshold, the circuit transitions to Open. - Open — requests are rejected immediately (fast-fail) with
503 Service Unavailable, aRetry-Afterheader, and a JSON body{"error":"circuit_open",...}. The backend is not contacted. The circuit stays open forBreakDuration. - Half-open — after
BreakDurationelapses, the next request is allowed through as a probe. If it succeeds the circuit returns to Closed; if it fails the circuit re-opens for anotherBreakDuration.
Timeouts are enforced per request via a linked cancellation token; a timed-out request
returns 504 Gateway Timeout and counts as a failure. Backend connection errors return
502 Bad Gateway. Set CircuitBreakerEnabled = false on a policy to keep timeouts but
disable the breaker, or Enabled = false at the root to bypass resilience entirely.
Relationship to Pragmatic.Agent
Section titled “Relationship to Pragmatic.Agent”The gateway can run with only static routes, but it becomes much more useful when connected to the Agent:
- route and cluster definitions can be reloaded from Agent KV state
- maintenance state can be coordinated instead of being purely local
- gateway configuration can participate in the broader Pragmatic operational model
If the Agent is unavailable, the gateway continues with its configured static routes.
Authentication
Section titled “Authentication”Set Gateway:Jwt to enable JWT bearer authentication. The gateway always validates the token
signature; issuer and audience are validated when configured (a startup warning is logged if
either is absent, since leaving them open allows token reuse/forgery across issuers). A
SigningKey enables symmetric validation; a JwksUrl enables JWKS-based validation.
API key
Section titled “API key”Gateway:ApiKey binds to ApiKeyOptions (HeaderName, default X-Api-Key; and ValidKeys):
{ "Gateway": { "ApiKey": { "HeaderName": "X-Api-Key", "ValidKeys": ["${API_KEY_1}", "${API_KEY_2}"] } }}Load ValidKeys from a secrets manager or environment variables — never commit raw keys to
appsettings.json.
APPROFONDIRE: The
ApiKeysection is defined and bound intoGatewayOptions, but the current gateway host (Program.cs) does not yet wire up an API-key validation middleware — only JWT authentication is enforced today. ConfiguringApiKeyhas no runtime effect until that middleware is added. Treat this section as forward-looking configuration.
Set Gateway:Cors to enable a default CORS policy (CorsOptions):
{ "Gateway": { "Cors": { "Origins": ["https://app.example.com"], "Methods": ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], "Headers": ["*"], "AllowCredentials": true } }}Origins: ["*"] allows any origin only when AllowCredentials is false. Combining a
wildcard origin with AllowCredentials: true is invalid (the browser refuses it), so the
gateway fails fast at startup with an actionable message instead of an opaque ASP.NET error.
Rate limiting
Section titled “Rate limiting”Set Gateway:RateLimit to enable a global fixed-window limiter (RateLimitOptions). Requests
over the limit get 429 Too Many Requests:
{ "Gateway": { "RateLimit": { "PermitLimit": 1000, "Window": "00:01:00", "KeyStrategy": "ip" } }}PermitLimit requests are allowed per Window; KeyStrategy: "ip" partitions the limit by
client IP.
Tenant Routing and Header Security
Section titled “Tenant Routing and Header Security”TenantRoutingMiddleware runs after authentication and before the proxy. It resolves the
tenant from (1) the authenticated tenant_id claim, then (2) the request subdomain
(acme.myapp.com → acme, excluding reserved infrastructure subdomains such as www, api,
admin), and forwards it to backends as the X-Tenant-Id header.
Crucially, the middleware always strips any client-supplied X-Tenant-Id header first,
before resolution. The gateway is the authoritative source of this header so downstream
services can trust it. If the client value were honored, a user authenticated for tenant A
could read or write tenant B’s data simply by setting X-Tenant-Id: B — a cross-tenant
isolation breach. Only the gateway-computed value (from the signed claim or DNS-controlled
subdomain) is ever forwarded.
Key Types
Section titled “Key Types”| Type | Purpose |
|---|---|
GatewayOptions | Full runtime configuration for listen URLs, auth, routes, maintenance, and resilience |
AgentRouteProvider | YARP config provider backed by Agent KV plus static fallback |
MaintenanceMiddleware | Stops normal request flow when maintenance is active |
TenantRoutingMiddleware | Applies tenant-aware routing decisions before proxying |
ProxyResilienceMiddleware | Timeout and circuit-breaker protection per backend cluster |
Current Scope
Section titled “Current Scope”Implemented today:
- standalone gateway executable
- Agent-backed and static route loading
- maintenance mode
- health endpoint
- response compression
- JWT auth and CORS
- global rate limiting
- per-cluster resilience
Still evolving:
- broader deployment UX and packaging guidance
- deeper control-plane tooling around route authoring and rollout
Documentation
Section titled “Documentation”Local docs:
License
Section titled “License”Part of the Pragmatic.Design ecosystem — see Licensing. Pragmatic.Gateway is licensed under the PolyForm Small Business 1.0.0 license (free for small businesses; commercial license above the threshold).