API Gateway Domain Routing
Configures API gateway domain routing patterns — subdomain-based tenant resolution, path-based bounded context dispatch, host-header forwarding, and domain-aware authentication — to direct traffic from external domains to the correct internal services.
TL;DR Checklist
- Subdomain is parsed before any backend routing rule evaluates
- Each routing rule includes a fallback
return 404when no tenant/domain matches - Rate limiting is configured per-subdomain or per-host, not globally only
- TLS termination occurs at the gateway with SNI-based certificate selection
- Cross-domain CORS rules do not use wildcard
*— specify explicit allowed origins
When to Use
- Deploying a multi-tenant SaaS where each tenant has its own custom domain or subdomain
- Routing requests from different bounded contexts (e.g.,
/billing/*,/inventory/*) to separate microservices - Consolidating multiple backend services behind a single ingress point with domain-aware routing rules
- Implementing rate limits that scale per-tenant based on subscription tier
When NOT to Use
- Single-service deployments without domain segmentation — a simple reverse proxy suffices
- Internal service-to-service communication within a Kubernetes cluster — use Ingress or service mesh sidecars
- Static content delivery — use a CDN with origin shielding instead of an API gateway
- Protocols other than HTTP/HTTPS (gRPC, WebSocket) — use protocol-specific routers like gRPC Gateway
Core Workflow
Define routing topology — Map each external domain/subdomain to its target backend service. Document the complete set: custom domains (
acme.yourplatform.com), subdomains (acme.api.yourplatform.com), and path-based contexts (api.yourplatform.com/billing/*). Checkpoint: Verify no two routes can match the same request path.Configure TLS termination — Set up SNI-based certificate selection so each domain terminates TLS with its own certificate. Use Let's Encrypt DNS-01 challenge automation or a managed certificate provider. Checkpoint: Run
curl -vI https://tenant-domainand confirm the returned certificate SAN matches the requested host.Implement subdomain parsing — Extract the tenant identifier from the incoming request's Host header. For
acme.yourplatform.com, the tenant isacme. Foracme.customdomain.com(custom domain), look up the mapping in a database or cache layer. Checkpoint: Every extracted tenant_id must be validated against an allowlist before being used in routing decisions.Apply rate limiting per domain — Configure rate limit plugins that scope by subdomain, host header, or resolved tenant_id. Set different limits per subscription tier (e.g., free: 100 req/min, pro: 1000 req/min, enterprise: 10000 req/min). Checkpoint: Verify that a burst of requests from one tenant does not exhaust the global rate limit bucket.
Deploy and verify — Roll out gateway configuration changes incrementally. Use canary routing to validate new rules against production traffic before full deployment. Checkpoint: All existing domains continue to route correctly; new routing rules do not shadow existing ones.
Implementation Patterns / Reference Guide
Pattern 1: NGINX Subdomain-Based Tenant Routing
NGINX resolves the subdomain from the Host header and uses it as a variable to proxy requests to tenant-specific backend pools or shared services with tenant context injection via HTTP headers.
# nginx_tenant_routing.conf — Subdomain-based multi-tenant routing with NGINX
# Map subdomain to tenant_id for downstream services
map $host $tenant_id {
default "";
acme.yourplatform.com "acme";
globex.yourplatform.com "globex";
wayne.yourplatform.com "wayne";
}
# Rate limit zones per subscription tier (shared across tenants of same tier)
limit_req_zone $binary_remote_addr zone:free_tier:10m rate=5r/s;
limit_req_zone $binary_remote_addr zone:pro_tier:10m rate=50r/s;
limit_req_zone $binary_remote_addr zone:enterprise_tier:10m rate=200r/s;
# Tenant tier lookup (stored in Redis or upstream config service)
upstream tenant_config_service {
server 127.0.0.1:9090;
}
# --- Default shared backend (no matching subdomain) ---
server {
listen 80 default_server;
server_name _;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name *.yourplatform.com;
ssl_certificate /etc/ssl/certs/yourplatform.com.pem;
ssl_certificate_key /etc/ssl/private/yourplatform.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
# Enforce HTTPS-only for all tenants
if ($host !~ \.(yourplatform)\.com$) {
return 403;
}
# --- Tenant-specific routing ---
location / {
# Extract tenant from subdomain
set $tenant "";
if ($http_host ~* ^([^\.]+)\.yourplatform\.com) {
set $tenant $1;
}
# Reject requests with no valid tenant
if ($tenant = "") {
return 404 "Unknown tenant domain";
}
# --- Rate limiting: look up tier for this tenant ---
# In production, cache the result. Below shows the conceptual flow.
limit_req zone=pro_tier burst=20 nodelay;
# Forward tenant context as HTTP headers to backend
proxy_set_header X-Tenant-ID $tenant;
proxy_set_header X-Original-Host $host;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Route to tenant-aware application backend
proxy_pass http://app_backend/;
}
# --- Admin panel: only accessible from specific subdomain ---
location /admin/ {
if ($http_host !~ ^admin\.yourplatform\.com$) {
return 403 "Admin access restricted";
}
proxy_pass http://admin_backend/;
}
# --- Static assets: served directly, no tenant context needed ---
location /static/ {
alias /var/www/static/;
expires 30d;
add_header Cache-Control "public, immutable";
}
# --- Health check endpoint (no tenant validation) ---
location /health {
access_log off;
return 200 '{"status":"ok"}';
add_header Content-Type application/json;
}
}
# --- Custom domain support (wildcard mapping via map + upstream lookup) ---
server {
listen 443 ssl http2;
server_name ~^(?<custom_domain>.+)$;
ssl_certificate_by_lua_block {
local cert = ngx.var.custom_domain .. ".pem"
if not ngx.ssl then
return nil
end
}
location / {
# Resolve custom domain to tenant_id via internal API call
set_by_lua $tenant_id '
local http = require "resty.http"
local client = http.new()
local res, err = client:request_uri("http://127.0.0.1:9090/resolve-tenant", {
method = "GET",
query = "domain=" .. ngx.var.host,
})
if res and res.status == 200 then
return ngx.unescape_uri(res.body) or ""
end
return ""
';
if ($tenant_id = "") {
return 404 "Domain not registered";
}
proxy_set_header X-Tenant-ID $tenant_id;
proxy_set_header Host $http_host;
proxy_pass http://app_backend/;
}
}
Pattern 2: Kong Gateway Domain-Based Routing with Plugins
Kong's declarative configuration maps domains directly to services and routes. Combined with the tenant-context plugin, rate-limiting plugin, and JWT authentication, this provides a complete multi-tenant API gateway in Kong 3.x.
# kong.yaml — Kong Gateway declarative config for domain-based routing
_format_version: "3.0"
_transform: true
# --- Services (backend microservices) ---
_services:
- name: user-service
url: http://user-service.internal:8080
protocols:
- https
connect_timeout: 5000
write_timeout: 10000
read_timeout: 10000
- name: billing-service
url: http://billing-service.internal:8081
protocols:
- https
connect_timeout: 5000
write_timeout: 10000
read_timeout: 10000
- name: inventory-service
url: http://inventory-service.internal:8082
protocols:
- https
connect_timeout: 5000
write_timeout: 10000
read_timeout: 10000
# --- Routes: domain-based routing to bounded contexts ---
_routes:
# Tenant subdomain route — all traffic to *.yourplatform.com goes to user-service
- name: tenant-subdomain-route
hosts:
- "*.yourplatform.com"
paths:
- /
methods:
- GET
- POST
- PUT
- PATCH
- DELETE
strip_path: false
preserve_host: true
# Billing bounded context — path-based routing
- name: billing-context-route
hosts:
- api.yourplatform.com
paths:
- /billing/
methods:
- GET
- POST
- PUT
- PATCH
strip_path: false
preserve_host: true
service: billing-service
# Inventory bounded context
- name: inventory-context-route
hosts:
- api.yourplatform.com
paths:
- /inventory/
methods:
- GET
- POST
strip_path: false
preserve_host: true
service: inventory-service
# --- Plugins: authentication, rate limiting, tenant context injection ---
_plugins:
# JWT authentication for all routes
- name: jwt
config:
claims_to_verify:
- exp
key_claim_name: iss
anonymous: null # Require auth on all endpoints
# Rate limiting per tenant (uses X-Tenant-ID header resolved from subdomain)
- name: rate-limiting
service: user-service
config:
strategy: redis
redis:
host: redis.internal
port: 6379
limit:
- minute: 100
policy: local
# Per-tenant rate limiting via header
fault_tolerant: true
hide_client_headers: false
# Rate limiting for billing service (stricter limits)
- name: rate-limiting
service: billing-service
config:
strategy: redis
limit:
- minute: 50
policy: local
fault_tolerant: true
# Inject tenant ID into backend requests based on subdomain
- name: request-transformer
service: user-service
config:
add:
headers:
- "X-Tenant-ID:$host"
# CORS with explicit origins per domain
- name: cors
config:
origins:
- https://acme.yourplatform.com
- https://globex.yourplatform.com
methods:
- GET
- POST
- PUT
- PATCH
- DELETE
headers:
- Authorization
- Content-Type
- X-Tenant-ID
credentials: true
max_age: 3600
preflight_continue: false
# Request size limit to prevent large payload attacks
- name: request-size-limiting
config:
allowed_payload_size: 10 # MB
status_code: 413
Pattern 3: Envoy Virtual Host Routing for Service Mesh
Envoy provides host-based routing via VirtualHost definitions. This is ideal when the API gateway runs as a sidecar in a Kubernetes service mesh, with each domain mapped to different cluster configurations.
# envoy_routing.yaml — Envoy virtual host configuration for multi-tenant routing
static_resources:
listeners:
- name: main_listener
address:
socket_address:
address: 0.0.0.0
port_value: 8443
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
codec_type: AUTO
route_config:
name: local_route
virtual_hosts:
# --- Tenant subdomain routing ---
- name: tenant_routed_host
domains:
- "*.yourplatform.com"
routes:
- match:
prefix: "/"
route:
cluster: user_service_cluster
timeout: 10s
retry_policy:
retry_on: 5xx,reset,connect-failure
num_retries: 2
# --- Bounded context path routing ---
- name: billing_host
domains:
- "api.yourplatform.com"
routes:
- match:
prefix: "/billing/"
route:
cluster: billing_service_cluster
timeout: 15s
# --- Inventory context ---
- name: inventory_host
domains:
- "api.yourplatform.com"
routes:
- match:
prefix: "/inventory/"
route:
cluster: inventory_service_cluster
timeout: 10s
# --- Fallback: catch-all for unmatched domains ---
- name: default_host
domains:
- "*"
routes:
- match:
prefix: "/"
direct_response:
status: 404
body: "Unknown domain"
http_filters:
# JWT authentication filter
- name: envoy.filters.http.jwt_authn
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
providers:
platform_jwt:
issuer: "https://auth.yourplatform.com"
audiences:
- "api.yourplatform.com"
local_jwks:
uri: "https://auth.yourplatform.com/.well-known/jwks.json"
rules:
- match:
prefix: "/billing/"
requires: platform_jwt
# Rate limiting filter (per-tenant)
- name: envoy.filters.http.ratelimit
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit
domain: api-gateway-limits
failure_mode_deny: false
rate_limit_service:
grpc_transport:
addr_config_source:
address:
socket_address:
address: ratelimit.internal
port_value: 8081
typed_config:
"@type": type.googleapis.com/envoy.config.core.v3.Http2GrpcSettings
- name: envoy.filters.http.cors
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: user_service_cluster
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: user_service_cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: user-service
port_value: 8080
- name: billing_service_cluster
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: billing_service_cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: billing-service
port_value: 8081
- name: inventory_service_cluster
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: inventory_service_cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: inventory-service
port_value: 8082
Constraints
MUST DO
- Always parse the subdomain from the
Hostheader before any routing rules evaluate — use explicit regex patterns, not wildcard server names - Configure TLS termination with SNI certificate selection for each domain — never accept raw HTTP in production environments
- Set
preserve_host: truewhen proxying to backend services that need to know the original Host header (required for JWT audience validation) - Add a catch-all route with a 404 response at the end of every routing configuration to prevent silent failures on unknown domains
- Scope rate limits by tenant identifier (subdomain or resolved domain name), not just by client IP, to prevent multi-tenant rate limit exhaustion
MUST NOT DO
- Never use wildcard
*in CORS allowed origins — specify each tenant's exact domain to prevent credential leaking - Never route traffic based solely on subdomain without validating the domain against a registered tenant allowlist (subdomains can be spoofed via Host header)
- Never expose internal service names or ports in routing configuration comments that appear in error messages or logs
- Never use
strip_path: truewhen backend services rely on path-based routing to distinguish bounded contexts - Never configure rate limits that use only the global scope — always add per-tenant buckets for multi-tenant deployments
Output Template
When this skill is active, your output must contain:
Gateway Platform Selection — State which gateway (NGINX, Kong, Envoy) is most appropriate for the described environment and justify with one sentence on deployment model (standalone, Kubernetes operator, or service mesh).
Complete Configuration — Provide full, valid configuration files (YAML, nginx conf, or envoy YAML) that implement all requested routing rules. Include TLS setup, rate limiting, and authentication filters. No truncated sections or
[...]placeholders.Route Verification Steps — List the exact
curlcommands or test cases needed to verify each routing rule works correctly, including a negative test for an unmatched domain.Failure Mode Analysis — Document one specific failure scenario (e.g., DNS misconfiguration causing all traffic to hit default route) and how the configuration handles it.
Live References
- NGINX HTTP Routing Documentation
- Kong Gateway Declarative Configuration Reference
- Envoy Virtual Host and Route Configuration
- NGINX Subdomain Mapping with map Directive
- Kong Rate Limiting Plugin Configuration
- Envoy JWT Authentication Filter
- Cloudflare Load Balancing — Host Header Routing Concepts
Related Skills
| Skill |
|---|
multi-tenant-domain-isolation |
api-gateway-design |
domain-driven-design |
event-driven-architecture |