api-gateway (Kong) — Application Logic
Status: populated Owner: TBD (Platform / SRE) Last updated: 2026-04-17 Companion: SERVICE_OVERVIEW · SECURITY_MODEL · Service Template
1. Purpose
Describe the request flow through Kong, the plugin chain ordering, error shaping, and the behaviour of the single custom Ghasi plugin. Kong has no application logic in the DDD sense; what follows is the edge middleware pipeline.
2. Request flow
3. Plugin chain (logical order)
Kong executes plugins in phase order (access, rewrite, header_filter, body_filter, log). Within a phase, plugins run by configured priority. The logical chain for a customer SMS-send request:
- TLS / SNI — Cloudflare (edge) then Kong (origin) terminate TLS. Minimum TLS 1.2; 1.3 preferred.
- Route match — host + path + method; rejects unknown routes with
404 Not Foundin Kong's default problem+json shape. jwtorkey-auth— authentication. Failure →401 Unauthorized.- Custom
ghasi-api-key-lookup(key-auth routes only, when enabled) — resolves the API key toaccount_id+scopes+tiervia cachedauth-servicecall; injectsX-Account-Id,X-Api-Key-Id,X-Tierheaders. ip-restriction— admin and partner routes only. Failure →403 Forbidden.request-size-limiting— default 64 KB for/v1/sms/send; 2 MB for/v1/sms/bulk. Failure →413 Payload Too Large.bot-detection— blocks obvious bot UAs on public routes. Failure →403 Forbidden.rate-limiting-advanced— Redis-backed counters keyed by consumer + window. Failure →429 Too Many RequestswithRetry-After.request-transformer— removeX-Powered-By, injectX-Gateway-Source: kong, propagateX-Tenant-Id.correlation-id— injectX-Request-Idif missing (UUIDv7); forward to upstream.opentelemetry— start span; injecttraceparentinto upstream request.- Proxy to upstream; upstream logic (validation, idempotency, NATS publish) runs in
sms-orchestrator. http-log— headers and metadata only to Loki; body logging disabled.
4. Rate limiting
Rate limiting is the single most load-bearing edge concern for a telecom SMS platform. Kong's rate-limiting-advanced plugin is configured with a Redis cluster backend.
| Tier | Key | Window | Default limit |
|---|---|---|---|
| Global | kong:rl:global | 1 s | 5 000 req/s (burst guard) |
| Per API key | kong:rl:key:<api_key_id> | 1 s / 60 s | Per-tier (from auth-service) |
| Per account | kong:rl:acct:<account_id> | 60 s | Per-tier aggregated |
| Per operator (downstream hint) | kong:rl:op:<operator_id> | 1 s | Configured in operator-management-service; read by Kong at config time |
Fail mode: fail-closed for /v1/sms/send (safer to reject than double-charge). Fail-open for read-only endpoints (/v1/sms/{id}, /v1/analytics/*). Configured per route.
Headers returned on throttled responses:
Retry-After: <seconds>X-RateLimit-Limit-<window>,X-RateLimit-Remaining-<window>,X-RateLimit-Reset-<window>
5. Custom plugin: ghasi-api-key-lookup
Purpose: Resolve an X-Api-Key header to an account context without pre-provisioning a Kong Consumer row per customer. Useful when API-key issuance is high-volume or churny (customer-initiated rotation).
Behaviour:
- Read
X-Api-Keyheader. If absent → defer tokey-authplugin (fail). - Hash the key (
sha256), check in-memory LRU cache (TTL 60 s, max 10 000 entries). - On miss →
GET {auth-service}/internal/api-keys/resolve?hash=<hash>with service-to-service auth (mTLS or service token). auth-servicereturns{ accountId, apiKeyId, scopes[], tier, status }or404.- On
status != "active"or404→ reject with401 invalid_api_key. - On success → populate Kong context:
kong.ctx.shared.consumer = { username: "csm-<accountId>", custom_id: "<accountId>" }- Inject headers:
X-Account-Id,X-Api-Key-Id,X-Tier. - Attach rate-limit key overrides via shared context for
rate-limiting-advanced.
- Emit Prometheus counters:
ghasi_api_key_lookup_total{result="hit|miss|invalid"}and histogramghasi_api_key_lookup_latency_seconds.
Failure modes:
auth-serviceunavailable: plugin returns503withRetry-After: 1(do not fail-open — an unauthenticated request must never reachsms-orchestrator).- Cache hot, auth-service momentarily down: serve from cache while TTL unexpired; log a warning.
Implementation note: Code lives in the application monorepo at ops/kong/plugins/ghasi-api-key-lookup/. Language: Lua (Kong plugin SDK) or Go (via Kong go-pdk) — SRE choice. This doc is the contract.
6. Error shaping
Kong's default error format is terse JSON ({ "message": "..." }). We configure the response-transformer-advanced (or a small error-handler plugin) so edge errors use Problem+json aligned with docs/standards/ERROR_CODES.md:
{
"type": "https://errors.ghasi.io/gateway/rate-limited",
"title": "Too Many Requests",
"status": 429,
"detail": "Per-key limit of 10 req/s exceeded.",
"instance": "urn:gw:<X-Request-Id>",
"retryAfter": 1
}
Upstream errors pass through unchanged — upstream services are the source of truth for their own error bodies.
7. Header contract (ingress → upstream)
| Header | Direction | Set by | Notes |
|---|---|---|---|
Authorization: Bearer <jwt> | in → upstream | Client | Forwarded verbatim |
X-Api-Key | in | Client | Not forwarded upstream (stripped after auth) |
X-Account-Id | out (kong → upstream) | ghasi-api-key-lookup or jwt plugin | Trusted by upstream only when from Kong |
X-Api-Key-Id | out | ghasi-api-key-lookup | For audit |
X-Tier | out | ghasi-api-key-lookup or JWT claim | For billing/ratelimit visibility |
X-Request-Id | in/out | correlation-id | Generated if absent |
traceparent | in/out | opentelemetry | W3C trace context |
X-Tenant-Id | in/out | Client or JWT tid | Propagated |
Idempotency-Key | in → upstream | Client | Forwarded verbatim to sms-orchestrator |
X-Gateway-Source: kong | out | request-transformer | Lets upstream distinguish Kong vs direct access |
X-Forwarded-For | out | Kong built-in | Chain: client, CF, kong |
Upstream services must trust X-Account-Id only when X-Gateway-Source: kong is present AND the request arrived on the internal network (zero-trust: verify via mTLS or network policy). East-west traffic that bypasses Kong uses service tokens instead.
8. Idempotency
Kong does not implement idempotency. The Idempotency-Key header is forwarded verbatim to sms-orchestrator, which owns the Redis-backed dedupe store (see services/sms-orchestrator/APPLICATION_LOGIC.md).
9. NATS publishing
Kong does not publish NATS events. Any upstream service consuming a request publishes its own events per EVENT_SCHEMAS and its respective service docs.
10. Health and readiness
/status— Kong's internal status endpoint (Admin API only, network-isolated)./health(on proxy port) — a custom dummy Route returning200when Kong data plane is alive; scraped by the Kubernetes liveness probe.- Readiness probe: Kong exits unready if JWKS fetch has failed continuously for N minutes (configurable).
11. Open questions
-
rate-limiting-advancedvs stockrate-limitingplugin — enterprise vs OSS decision. - Custom plugin language (Lua vs Go). Go is easier to test; Lua has lower overhead.
- Fail-closed vs fail-open per route — explicit matrix pending (SMS endpoints closed; analytics open).