CRUMB a card from devarno-cloud

Service Port Map & Header Forwarding

tektree beginner 3 min read

ELI5

Each booth in the food court has its own back-door number (8081 to 8085). The host (gateway) is the only door customers use — they never knock on a booth’s back door. When the host walks an order back, they staple a sticky note: “user 42, pro tier.”

Technical Deep Dive

Port Allocation

flowchart LR
Client((Client)) -->|443/80| GW[api-gateway :8080]
GW -->|HTTP| US[user-service :8081]
GW -->|HTTP| KS[knowledge-service :8082]
GW -->|HTTP| GS[gamification-service :8083]
GW -->|HTTP| PS[payment-service :8084]
GW -->|WS upgrade| RT[realtime-service :8085]
WP[worker-pool : no HTTP] -.->|XREADGROUP jobs| Redis[(Redis Streams)]
Legacy[services/api :8000] -.->|standalone| Mongo[(MongoDB)]
ServicePortDirect HTTP exposure outside cluster?
api-gateway8080Yes (the only one)
user-service8081No
knowledge-service8082No
gamification-service8083No
payment-service8084No (Polar webhooks reach it via the gateway / ingress rule)
realtime-service8085No (gateway handles WS upgrade then proxies)
worker-poolNo HTTP
services/api (legacy)8000Pre-gateway monolith; not behind the new gateway

Header Contract

The gateway’s ProxyTo (services/api-gateway/internal/handlers/handlers.go:58-62) injects:

HeaderSourceUsed by
X-User-IDJWT sub claimEvery protected downstream as the caller identity
X-User-TierJWT tier claimDownstreams that gate features by plan
X-Forwarded-Hostoriginal HostLogging / audit
X-Forwarded-ProtoschemeURL builders
X-Request-IDRequestID middlewareTrace correlation

Downstream Go services read these via c.GetHeader("X-User-ID") in their handlers — they do not parse the JWT themselves. A request that arrives at a downstream port directly (bypassing the gateway) therefore has no authentication: do not expose those ports outside the cluster.

Local Development

infra/infra/docker/ ships docker-compose files that bring up Mongo + Redis and the services on these ports. To call a service directly during local debugging, hit http://localhost:8081/api/v1/users/<id> and add X-User-ID / X-User-Tier headers manually — the gateway is bypassed and so is auth.

Key Terms

  • Public port:8080 only (the gateway).
  • Internal-only port → 8081–8085 + 8000 (legacy); accessible inside the cluster network.
  • X-User-ID / X-User-Tier → gateway-injected identity headers; the downstream’s source of truth.
  • No-HTTP serviceworker-pool consumes Redis Streams and exposes no listener.

Q&A

Q: A new service needs to call user-service synchronously. Should it use port 8081 or go through the gateway? A: Service-to-service calls go to the internal port (8081) directly. The gateway is the public ingress; using it for internal hops doubles the auth/proxy latency and breaks the trust assumption (the new service would need its own JWT).

Q: Why does worker-pool have no port? A: It is a pull-based consumer (XREADGROUP on Redis Stream jobs). Health is observed via Prometheus scrape on its metrics endpoint and via stream lag, not an HTTP /health endpoint exposed publicly.

Q: A request to legacy services/api on :8000 still works. Does it use the new auth? A: No. The legacy monolith parses the access_token cookie itself (services/api/middleware/auth.middleware.go). It is not behind the new gateway and does not consume the X-User-ID headers.

Examples

Hitting knowledge-service directly during a unit test: curl -H 'X-User-ID: 64a...' -H 'X-User-Tier: pro' http://localhost:8082/api/areas — same response shape the gateway would have proxied.

neighbors on the map