API Gateway

HTTP routing, authentication middleware, rate limiting, service dispatch, health checks, and voice relay in Aether's API gateway.

Every request entering the Aether platform passes through the API gateway. The aether-gateway crate provides HTTP routing, authentication validation, rate limiting, service dispatch, health monitoring, and request metrics as composable, framework-agnostic building blocks.

Key Concepts

  • Router -- Path and method matching with parameterized segments and wildcards.
  • Auth middleware -- JWT token validation with expiry, issuer, and signature checks.
  • Rate limiting -- Token-bucket algorithm per user and route pair.
  • Service dispatch -- Maps matched routes to backend service targets.
  • Health checks -- Per-service state machine tracking availability.
  • Metrics -- Request counters, latency tracking, and error rates.

Architecture

The gateway processes requests through a pipeline of stages:

Incoming Request
       |
       v
  +---------+     +------------+     +----------+     +---------+
  | Router  | --> | Middleware  | --> | Dispatch | --> | Backend |
  +---------+     | (auth,     |     +----------+     | Service |
                  |  rate-limit)|                      +---------+
                  +------------+
                        |
                  +----------+
                  | Metrics  |
                  +----------+
                        |
                  +----------+
                  | Health   |
                  +----------+

All types are library-level Rust APIs with no HTTP server dependency. The routing logic is abstract so it can be plugged into axum, actix, or any other framework.

Route Configuration

Routes define path patterns, HTTP methods, target services, and access control:

use aether_gateway::{Router, Route, HttpMethod, ServiceTarget, RateLimitRule};

let mut router = Router::new();

router.add_route(Route {
    path_pattern: "/api/v1/worlds/:world_id".to_string(),
    method: HttpMethod::Get,
    service: ServiceTarget::WorldServer { zone_id: String::new() },
    auth_required: true,
    rate_limit: Some(RateLimitRule {
        tokens_per_second: 10.0,
        burst_capacity: 20,
    }),
});

router.add_route(Route {
    path_pattern: "/api/v1/auth/login".to_string(),
    method: HttpMethod::Post,
    service: ServiceTarget::AuthService,
    auth_required: false,
    rate_limit: Some(RateLimitRule {
        tokens_per_second: 1.0,
        burst_capacity: 5,
    }),
});

Path matching supports three patterns:

PatternExampleDescription
Exact/api/v1/healthMatches the exact path
Parameterized/api/v1/worlds/:world_idCaptures named segments
Wildcard/api/v1/ugc/*Matches any suffix

Routes are evaluated in registration order; the first match wins.

Request Matching

The router extracts path parameters from matched routes:

use aether_gateway::RouteMatch;

if let Some(route_match) = router.match_request(HttpMethod::Get, "/api/v1/worlds/abc-123") {
    // route_match.route -> the matched Route
    // route_match.params -> {"world_id": "abc-123"}
    let world_id = route_match.params.get("world_id").unwrap();
}

Unmatched requests return None, which the HTTP layer can map to a 404 response.

Authentication Middleware

The auth middleware validates JWT tokens against a configurable policy:

use aether_gateway::{AuthMiddleware, AuthValidationPolicy, AuthzResult, Token};

let policy = AuthValidationPolicy {
    allowed_issuers: vec!["aether-identity".to_string()],
    require_signature: true,
};

let auth = AuthMiddleware::new(policy);

let token = Token {
    subject: "user-uuid".to_string(),
    issuer: "aether-identity".to_string(),
    expires_at_ms: 1709890800000,
    signature_valid: true,
};

match auth.validate(&token, now_ms) {
    AuthzResult::Allowed { user_id } => {
        // Proceed with request
    }
    AuthzResult::Denied { reason } => {
        // Return 401/403
    }
    AuthzResult::Expired => {
        // Token expired, return 401
    }
}

Validation checks:

  • Token expiry against current time.
  • Issuer is in the allowed issuers list.
  • Signature validity flag (actual cryptographic verification happens upstream).

Rate Limiting

The rate limiter uses a token-bucket algorithm per (user, route) pair:

use aether_gateway::{RateLimiter, RateLimitRule, RateLimitStatus};

let mut limiter = RateLimiter::new();

let rule = RateLimitRule {
    tokens_per_second: 10.0,
    burst_capacity: 20,
};

match limiter.check(user_id, "worlds-list", &rule, now_ms) {
    RateLimitStatus::Allowed { remaining } => {
        // Request permitted, `remaining` tokens left
    }
    RateLimitStatus::Limited { retry_after_ms } => {
        // Return 429 with Retry-After header
    }
}

Each bucket tracks remaining tokens and last refill time. Tokens are refilled based on elapsed time up to the burst capacity.

Service Dispatch

The dispatcher maps matched routes to backend service targets:

use aether_gateway::{Dispatcher, DispatchContext, ServiceTarget};

let dispatcher = Dispatcher::new();

let context: DispatchContext = dispatcher.dispatch(&route_match, Some(user_id));

match context.target {
    ServiceTarget::WorldServer { zone_id } => {
        // Forward to world server for this zone
    }
    ServiceTarget::AuthService => {
        // Forward to identity/auth service
    }
    ServiceTarget::SocialService => {
        // Forward to social service
    }
    ServiceTarget::EconomyService => {
        // Forward to economy service
    }
    ServiceTarget::UgcService => {
        // Forward to UGC service
    }
    ServiceTarget::RegistryService => {
        // Forward to world registry
    }
}

The DispatchContext carries extracted path parameters, the authenticated user ID, and the target service for downstream processing.

Health Checks

The health checker tracks per-service availability through a state machine:

use aether_gateway::{HealthChecker, HealthCheckConfig, ServiceHealthState};

let config = HealthCheckConfig {
    degraded_threshold: 3,   // consecutive failures to enter Degraded
    unhealthy_threshold: 5,  // consecutive failures to enter Unhealthy
};

let mut health = HealthChecker::new(config);

// Report check results
health.report_success("world-server", now_ms);
health.report_failure("economy-service", now_ms);

// Query service health before dispatching
match health.status("economy-service") {
    ServiceHealthState::Healthy => { /* dispatch normally */ }
    ServiceHealthState::Degraded => { /* dispatch with caution, maybe shed load */ }
    ServiceHealthState::Unhealthy => { /* return 503 */ }
}

Health state transitions:

  • Healthy -> Degraded after reaching the degraded failure threshold.
  • Degraded -> Unhealthy after reaching the unhealthy failure threshold.
  • Any state -> Healthy on a successful check.

Request Metrics

In-memory metrics track request volume, latency, and error rates per route:

use aether_gateway::{RequestMetrics, RouteMetricsSnapshot};

let mut metrics = RequestMetrics::new();

// Record after each request completes
metrics.record("worlds-list", latency_ms, /* success */ true);
metrics.record("worlds-list", latency_ms, /* success */ false);

// Take a snapshot for monitoring
if let Some(snapshot) = metrics.snapshot("worlds-list") {
    println!("Total: {}", snapshot.total_requests);
    println!("Errors: {}", snapshot.error_count);
    println!("Avg latency: {}ms", snapshot.average_latency_ms);
    println!("Max latency: {}ms", snapshot.max_latency_ms);
}

Metrics are plain Rust data structures with no external dependencies, suitable for export to any observability system.

Voice Relay

The gateway also manages voice relay sessions for spatial audio. Relay profiles handle STUN/TURN NAT traversal and regional routing, ensuring voice traffic is directed to the closest edge node for minimal latency.

Full Request Flow

A complete request through the gateway follows this sequence: route matching (extract path params) -> auth validation (JWT check if required) -> rate limit check (token bucket) -> health check (service availability) -> dispatch (forward to backend) -> record metrics (latency and success/failure).