Waygate Server Guide¶
The Waygate Server is a standalone ASGI application that acts as the centralised control plane for multi-service architectures. Services connect to it via WaygateSDK, which enforces rules locally using an in-process cache kept fresh over a persistent SSE connection — so there is zero per-request network overhead.
When to use Waygate Server¶
Use the embedded WaygateAdmin (single-mount) pattern when you have one service. Move to Waygate Server when:
- You run two or more independent services and want to manage all their routes from one dashboard and one CLI target.
- You want a dedicated control plane that survives individual service restarts.
- You need per-service namespacing so
payments-service:/api/paymentsandorders-service:/api/paymentsnever collide.
Architecture¶
graph TD
CB["CLI / Browser"]
subgraph server["Waygate Server • port 9000"]
SS["WaygateServer(backend=...)"]
UI["/ Dashboard UI"]
API["/api/... REST API"]
SSE["/api/sdk/events SSE stream"]
end
CB --> SS
SS --- UI
SS --- API
SS --- SSE
SSE -->|HTTP + SSE| P
SSE -->|HTTP + SSE| O
SSE -->|HTTP + SSE| X
subgraph P["payments-app • port 8000"]
PS["WaygateSDK\napp_id='payments-svc'\nlocal cache"]
end
subgraph O["orders-app • port 8002"]
OS["WaygateSDK\napp_id='orders-svc'\nlocal cache"]
end
subgraph X["any-other-app"]
XS["WaygateSDK\napp_id='...'\nlocal cache"]
end
Key properties:
- The Waygate Server is the single source of truth for route state.
- SDK clients pull state on startup via
GET /api/routesand receive live updates viaGET /api/sdk/events(SSE). - All enforcement happens locally inside each SDK client — the Waygate Server is never on the hot path.
- The CLI always points at the Waygate Server, never at individual services.
Quick setup¶
1. Waygate Server¶
from waygate.server import WaygateServer
from waygate import MemoryBackend
# MemoryBackend is fine for development.
# Use FileBackend for state persistence or RedisBackend for HA.
waygate_app = WaygateServer(
backend=MemoryBackend(),
auth=("admin", "secret"),
# secret_key="stable-key-for-persistent-tokens",
# token_expiry=86400, # 24 h
)
2. Service app¶
from fastapi import FastAPI
from waygate.sdk import WaygateSDK
from waygate.fastapi import WaygateRouter, maintenance, force_active
sdk = WaygateSDK(
server_url="http://localhost:9000",
app_id="payments-service",
# token="<long-lived-service-token>",
reconnect_delay=5.0,
)
app = FastAPI()
sdk.attach(app) # wires WaygateMiddleware + startup/shutdown hooks
router = WaygateRouter(engine=sdk.engine)
@router.get("/health")
@force_active
async def health():
return {"status": "ok"}
@router.get("/api/payments")
@maintenance(reason="Payment processor upgrade")
async def get_payments():
return {"payments": []}
app.include_router(router)
3. CLI¶
waygate config set-url http://localhost:9000
waygate login admin # password: secret
waygate services # list all connected services
waygate status --service payments-service
waygate enable /api/payments
sdk.attach(app) — what it does¶
attach() wires three things into the FastAPI app:
WaygateMiddleware— checks every request against the local state cache (zero network overhead).- Startup hook — syncs state from the Waygate Server, opens the SSE listener, discovers
@waygate_meta__-decorated routes, and registers any new ones with the server. - Shutdown hook — closes the SSE connection and HTTP client cleanly.
Multi-service CLI workflow¶
WAYGATE_SERVICE env var¶
Set once, and every subsequent command (status, enable, disable, maintenance, schedule) scopes itself to that service automatically:
export WAYGATE_SERVICE=payments-service
waygate status # only payments routes
waygate disable GET:/api/payments --reason "hotfix"
waygate enable GET:/api/payments
waygate current-service # confirm active context
An explicit --service flag always overrides the env var:
export WAYGATE_SERVICE=payments-service
waygate status --service orders-service # acts on orders, ignores env var
Unscoped commands¶
Clear WAYGATE_SERVICE to operate across all services at once:
unset WAYGATE_SERVICE
waygate status # routes from all services
waygate audit # audit log across all services
waygate global disable --reason "emergency maintenance"
waygate global enable
Global maintenance in multi-service environments¶
waygate global enable and waygate global disable operate on the Waygate Server and affect every route across every connected service simultaneously. Use this for fleet-wide outages or deployments where all services must be taken offline at once.
# Block all routes on all services
waygate global enable --reason "Deploying v3" --exempt /health --exempt /ready
# Restore all routes
waygate global disable
Per-service maintenance¶
Per-service maintenance puts all routes of one service into maintenance mode without affecting other services. SDK clients with a matching app_id receive the sentinel over SSE and treat all their routes as in maintenance — no individual route changes needed.
From the engine (programmatic)¶
# Put payments-service into maintenance
await engine.enable_service_maintenance(
"payments-service",
reason="DB migration",
exempt_paths=["/health"],
)
# Restore
await engine.disable_service_maintenance("payments-service")
# Inspect
cfg = await engine.get_service_maintenance("payments-service")
print(cfg.enabled, cfg.reason)
From the REST API¶
POST /api/services/payments-service/maintenance/enable
Authorization: Bearer <token>
Content-Type: application/json
{"reason": "DB migration", "exempt_paths": ["/health"]}
From the CLI¶
waygate sm and waygate service-maintenance are aliases for the same command group:
# Enable — all routes of payments-service return 503
waygate sm enable payments-service --reason "DB migration"
waygate sm enable payments-service --reason "Upgrade" --exempt /health
# Check current state
waygate sm status payments-service
# Restore
waygate sm disable payments-service
From the dashboard¶
Open the Routes page and select the service using the service filter. A Service Maintenance card appears with Enable and Disable controls.
Use @force_active on health checks
Health and readiness endpoints should always be decorated with @force_active so they are never affected by global or per-service maintenance mode.
Useful discovery commands¶
waygate services # list all services registered with the Waygate Server
waygate current-service # show the active WAYGATE_SERVICE context
Backend choice for the Waygate Server¶
The backend affects only the Waygate Server process. SDK clients always receive live updates via SSE regardless of which backend the Waygate Server uses.
| Scenario | Waygate Server backend | Notes |
|---|---|---|
| Development / single developer | MemoryBackend |
State lost on Waygate Server restart |
| Production, single Waygate Server | FileBackend |
State survives restarts; single process only |
| Production, multiple Waygate Server nodes (HA) | RedisBackend |
All nodes share state via pub/sub |
# Development
waygate_app = WaygateServer(backend=MemoryBackend(), auth=("admin", "secret"))
# Production — persistent single node
from waygate import FileBackend
waygate_app = WaygateServer(backend=FileBackend("waygate-state.json"), auth=("admin", "secret"))
# Production — HA / load-balanced Waygate Server
from waygate import RedisBackend
waygate_app = WaygateServer(backend=RedisBackend("redis://redis:6379/0"), auth=("admin", "secret"))
Shared rate limit counters across SDK replicas¶
When a service runs multiple replicas (e.g. three payments-app pods), each SDK client enforces rate limits independently using its own in-process counters. This means a 100/minute limit is enforced as 100 per replica — effectively 100 × num_replicas across the fleet.
To enforce the limit across all replicas combined, pass a shared RedisBackend as rate_limit_backend:
from waygate import RedisBackend
sdk = WaygateSDK(
server_url="http://waygate-server:9000",
app_id="payments-service",
rate_limit_backend=RedisBackend(url="redis://redis:6379/1"),
)
Use a different Redis database (or a different Redis instance) from the one used by the Waygate Server's backend to avoid key collisions.
Full deployment matrix¶
| Services | Replicas per service | Waygate Server backend | SDK rate_limit_backend |
|---|---|---|---|
| 1 | 1 | Use embedded WaygateAdmin instead |
— |
| 2+ | 1 each | MemoryBackend or FileBackend |
not needed |
| 2+ | 2+ each (shared counters) | RedisBackend |
RedisBackend (different DB) |
| 2+ | 2+ each (independent counters per replica) | RedisBackend |
not needed |
Per-service rate limits¶
A per-service rate limit applies a single policy to all routes of one service without configuring each route individually. It is checked after the all-services global rate limit and before any per-route limit:
flowchart LR
GM["global\nmaintenance"] --> SM["service\nmaintenance"] --> GL["global\nrate limit"] --> SL["service\nrate limit"] --> RL["per-route\nrate limit"]
Configure it with the waygate srl CLI (also available as waygate service-rate-limit):
# Cap all payments-service routes at 1000 per minute per IP
waygate srl set payments-service 1000/minute
# With options
waygate srl set payments-service 500/minute --algorithm sliding_window --key ip
waygate srl set payments-service 2000/hour --burst 50 --exempt /health --exempt GET:/metrics
# Inspect, pause, reset counters, remove
waygate srl get payments-service
waygate srl disable payments-service
waygate srl enable payments-service
waygate srl reset payments-service
waygate srl delete payments-service
From the dashboard: open the Rate Limits tab and select a service using the service filter. A Service Rate Limit card appears above the policies table with controls to configure, pause, reset, and remove the policy.
The service rate limit uses the same GlobalRateLimitPolicy model as the all-services global rate limit. It is stored in the backend under a sentinel key and survives Waygate Server restarts on FileBackend or RedisBackend.
Independent layers
The all-services global rate limit (waygate grl) and the per-service rate limit (waygate srl) are independent. A request must pass both before reaching per-route checking. You can configure one, both, or neither.
SSE event types¶
The Waygate Server's GET /api/sdk/events stream carries two event types:
Route state changes¶
Emitted whenever a route is enabled, disabled, put in maintenance, etc. The SDK client updates its local cache and the change is visible on the very next engine.check() call.
Rate limit policy changes¶
data: {"type": "rl_policy", "action": "set", "key": "GET:/api/pay", "policy": {...}}
data: {"type": "rl_policy", "action": "delete", "key": "GET:/api/pay"}
Emitted whenever waygate rl set or waygate rl delete (or the dashboard) changes a rate limit policy. The SDK client applies the change to its local engine._rate_limit_policies dict immediately via the existing _run_rl_policy_listener() background task.
Both event types are delivered over the same persistent SSE connection. Propagation latency is the SSE round-trip — typically under 5 ms on a LAN.
Authentication¶
No auth (development)¶
If auth is omitted, the Waygate Server is open — no credentials required anywhere:
sdk = WaygateSDK(
server_url="http://waygate-server:9000",
app_id="payments-service",
# no token, username, or password needed
)
Dashboard and CLI users¶
Use auth=("admin", "secret") for a single admin user, or pass a list for multiple users:
Human users authenticate via the dashboard login form or waygate login in the CLI. Their sessions last token_expiry seconds (default 24 h).
SDK service authentication¶
SDK service apps have two options for authenticating with the Waygate Server.
Option 1 — Auto-login with credentials (recommended)¶
Pass username and password directly to WaygateSDK. On startup the SDK calls POST /api/auth/login with platform="sdk" and obtains a long-lived token automatically — no manual token management required:
import os
from waygate.sdk import WaygateSDK
sdk = WaygateSDK(
server_url=os.environ["WAYGATE_SERVER_URL"],
app_id="payments-service",
username=os.environ["WAYGATE_USERNAME"],
password=os.environ["WAYGATE_PASSWORD"],
)
Store credentials in your secrets manager or CI/CD environment variables and inject them at deploy time. The token is obtained once on startup and lives for sdk_token_expiry seconds (default 1 year) so the service never needs re-authentication.
Option 2 — Pre-issued token¶
Generate a token once via the CLI and pass it directly:
waygate config set-url http://waygate-server:9000
waygate login admin
# Copy the token from ~/.waygate/config.json
sdk = WaygateSDK(
server_url="http://waygate-server:9000",
app_id="payments-service",
token=os.environ["WAYGATE_TOKEN"],
)
Separate token lifetimes¶
token_expiry and sdk_token_expiry are independent so human sessions can be short while service tokens are long-lived:
waygate_app = WaygateServer(
backend=RedisBackend(os.environ["REDIS_URL"]),
auth=("admin", os.environ["WAYGATE_ADMIN_PASSWORD"]),
secret_key=os.environ["WAYGATE_SECRET_KEY"],
token_expiry=3600, # dashboard / CLI users: 1 hour
sdk_token_expiry=31536000, # SDK service tokens: 1 year (default)
)
| Token platform | Issued to | Expiry param | Default |
|---|---|---|---|
"dashboard" |
Browser sessions | token_expiry |
24 h |
"cli" |
waygate login / CLI operators |
token_expiry |
24 h |
"sdk" |
WaygateSDK auto-login |
sdk_token_expiry |
1 year |
Production checklist¶
- [ ] Use a stable
secret_keyonWaygateServerso issued tokens survive Waygate Server restarts - [ ] Prefer
username/passwordonWaygateSDKover pre-issued tokens — the SDK self-authenticates with asdk_token_expirytoken on each startup - [ ] Set
token_expiry(human sessions) andsdk_token_expiry(service tokens) independently - [ ] Use
FileBackendorRedisBackendon the Waygate Server so route state survives restarts - [ ] Use
RedisBackendon the Waygate Server if you run more than one Waygate Server instance (HA) - [ ] Pass
rate_limit_backend=RedisBackend(...)to each SDK if you need shared counters across replicas of the same service - [ ] Set
reconnect_delayonWaygateSDKto a value appropriate for your network (default 5 s) - [ ] Exempt health and readiness probe endpoints with
@force_active - [ ] Test fail-open behaviour by stopping the Waygate Server and verifying that SDK clients continue to serve traffic against their last-known cache
Further reading¶
- Distributed Deployments → — deep-dive on
MemoryBackend,FileBackend,RedisBackendinternals and the pub/sub patterns they use - FastAPI adapter examples → — runnable
waygate_server.pyandmulti_service.pyexamples - CLI reference → — all commands including
waygate servicesandwaygate current-service