Skip to content

Gateway Routing

1. Overview

Routing in BANA uses the Service-Owned Prefix pattern. Each service sets its own base path (/v1/api/<service>) and declares its Traefik route via Docker labels. No central route configuration exists.

Source: Docker labels from infrastructure/deployments/develop/*/docker-compose.yml, static config from packages/gateway/config/traefik.yml

2. Edge → Traefik → Middleware Pipeline

Traffic enters through Nginx (TLS termination), then Traefik receives it on port :30080. Two configuration providers feed Traefik: Docker Provider reads container labels for routers/services, File Provider reads middlewares.yml for shared middleware definitions.

ComponentSourceDetails
NginxEdge layer (:80/:443)TLS termination, adds X-Forwarded-For/X-Real-IP
EntrypointTraefik web (:80)Receives HTTP from Nginx on host port :30080
rate-limit@filemiddlewares.yml200 req/s per-IP, burst: 400, ipStrategy.depth: 1
circuit-breaker@filemiddlewares.ymlnet error > 10% or P95 latency > 3s (5xx-ratio term commented out)
security-headers@filemiddlewares.ymlXSS filter, nosniff, frame deny, strip Server header
Docker Provider/var/run/docker.sockReads traefik.* labels, auto-creates routers + services
File Providerconfig/dynamic/middlewares.ymlShared middlewares, dashboard router + basic auth, hot reload

3. Router → Service Mapping

After passing through the middleware chain, Traefik matches the request to one of the registered routers. Each router is auto-discovered from Docker labels. The matched router forwards to its corresponding backend service on the Docker network.

RouterRuleMiddlewaresNotes
identityPathPrefix(/v1/api/identity)rate-limit-auth@file, circuit-breaker@file, security-headers@fileStricter auth rate limit
commercePathPrefix(/v1/api/commerce)rate-limit@file, circuit-breaker@file, security-headers@file
salePathPrefix(/v1/api/sale)rate-limit@file, circuit-breaker@file, security-headers@file
financePathPrefix(/v1/api/finance)rate-limit@file, circuit-breaker@file, security-headers@file
inventoryPathPrefix(/v1/api/inventory)rate-limit@file, circuit-breaker@file, security-headers@file
paymentPathPrefix(/v1/api/payment)rate-limit@file, circuit-breaker@file, security-headers@file
signal-restPathPrefix(/v1/api/signal)rate-limit@file, circuit-breaker@file, security-headers@file
signal-wsPathPrefix(/stream)WebSocket passthrough
payment-webhookHost(hook.bana.nexpando.vn)payment-add-prefix@docker, security-headers@filepriority: 100, path rewrite
portalPathPrefix(/)dashboard-auth@filepriority: 1, Portal dashboard

4. Traefik Components

ComponentSourceDescription
Docker Provider/var/run/docker.sockWatches running containers for traefik.* labels. Automatically creates routers and services when a container starts, and removes them when it stops.
File Providerconfig/dynamic/middlewares.ymlLoads shared middleware definitions (rate-limit, circuit-breaker, security-headers, dashboard-auth). Watches the file for changes — edits take effect without restarting Traefik.
RouterDocker labelsMatches incoming requests by rule (PathPrefix, Host). Each service container declares its own router. Traefik evaluates rules in priority order (default priority = rule string length). Use explicit priority when multiple routers can match the same request.
MiddlewareFile provider (@file) or Docker labels (@docker)Processes requests before reaching the backend. Shared middlewares live in file provider and must be referenced with @file suffix from Docker labels.
ServiceDocker labelsThe backend target — Traefik resolves the container IP via Docker network and forwards to the declared port. Each service includes health check configuration.
Health CheckDocker labelsTraefik actively polls /v1/api/<service>/health every 30 seconds. If a service fails, Traefik removes it from routing until health is restored.

5. Request Lifecycle

6. Three Routing Patterns

BANA uses three distinct routing patterns depending on the traffic type:

Pattern 1: Standard REST API

The most common pattern. A single router matches by path prefix and applies the full middleware chain.

Client → Nginx → Traefik
  → Router: PathPrefix(/v1/api/commerce)
  → Middlewares: rate-limit@file → circuit-breaker@file → security-headers@file
  → Service: dev-nx-commerce:3000

Used by: identity, commerce, sale, finance, inventory, payment, signal (REST).

Pattern 2: WebSocket (No Middlewares)

WebSocket connections require a persistent connection and must not be interrupted by rate limiting or circuit breaking. The signal-ws router has no middlewares.

Client → Nginx (Connection: upgrade) → Traefik
  → Router: PathPrefix(/stream)
  → Middlewares: (none)
  → Service: dev-nx-signal:3000

Traefik natively supports WebSocket — the Connection: Upgrade and Upgrade: websocket headers are forwarded automatically.

Pattern 3: 3rd-Party Webhook with Path Rewrite

For legacy 3rd-party integrations where the registered URL cannot be changed. A Host-based router matches by domain, and a replacepathregex middleware rewrites the path to add the service prefix.

VNPAY → Nginx (Host: hook.bana.nexpando.vn) → Traefik
  → Router: Host(hook.bana.nexpando.vn), priority: 100
  → Middlewares: payment-add-prefix@docker → security-headers@file
  → Path rewrite: /v1/api/payments/* → /v1/api/payment/payments/*
  → Service: dev-nx-payment:3000

The payment-add-prefix middleware is defined in Docker labels (not file provider) because it is specific to the payment service only.

Why priority: 100? Traefik's PathPrefix is a pure prefix match — PathPrefix(/v1/api/payment) also matches /v1/api/payments/... because /v1/api/payment is a prefix of /v1/api/payments. Without explicit priority, the payment router (PathPrefix) and payment-webhook router (Host) have similar default priorities (calculated from rule string length), and Traefik may route the webhook request through the wrong router — skipping the path rewrite. Setting priority: 100 ensures the Host-based webhook router always wins when Host: hook.bana.nexpando.vn is present.

7. Docker Label Configuration

Standard Service (Identity)

Source: infrastructure/deployments/develop/identity/docker-compose.yml

yaml
services:
  dev-nx-identity:
    labels:
      - "traefik.enable=true"
      # Router
      - "traefik.http.routers.identity.rule=PathPrefix(`/v1/api/identity`)"
      - "traefik.http.routers.identity.entrypoints=web"
      - "traefik.http.routers.identity.middlewares=rate-limit-auth@file,circuit-breaker@file,security-headers@file"
      # Service
      - "traefik.http.services.identity.loadbalancer.server.port=3000"
      - "traefik.http.services.identity.loadbalancer.healthcheck.path=/v1/api/identity/health"
      - "traefik.http.services.identity.loadbalancer.healthcheck.interval=30s"

WebSocket Service (Signal)

Source: infrastructure/deployments/develop/signal/docker-compose.yml

Signal service needs two routers — one for REST API, one for WebSocket:

yaml
services:
  dev-nx-signal:
    labels:
      - "traefik.enable=true"
      # REST API
      - "traefik.http.routers.signal-rest.rule=PathPrefix(`/v1/api/signal`)"
      - "traefik.http.routers.signal-rest.entrypoints=web"
      - "traefik.http.routers.signal-rest.middlewares=rate-limit@file,circuit-breaker@file,security-headers@file"
      # WebSocket
      - "traefik.http.routers.signal-ws.rule=PathPrefix(`/stream`)"
      - "traefik.http.routers.signal-ws.entrypoints=web"
      # Shared service
      - "traefik.http.services.signal.loadbalancer.server.port=3000"
      - "traefik.http.services.signal.loadbalancer.healthcheck.path=/v1/api/signal/health"
      - "traefik.http.services.signal.loadbalancer.healthcheck.interval=30s"

3rd-Party Webhook Router (Payment)

Source: infrastructure/deployments/develop/payment/docker-compose.yml

Payment service has two routers sharing one service. The second router handles 3rd-party webhooks from a legacy domain (hook.bana.nexpando.vn) where the registered URL cannot be changed. A replacepathregex middleware rewrites the path to include the /payment prefix.

yaml
services:
  dev-nx-payment:
    labels:
      - "traefik.enable=true"
      # Router — normal API traffic
      - "traefik.http.routers.payment.rule=PathPrefix(`/v1/api/payment`)"
      - "traefik.http.routers.payment.entrypoints=web"
      - "traefik.http.routers.payment.middlewares=rate-limit@file,circuit-breaker@file,security-headers@file"
      # Router — 3rd party webhook (legacy path without /payment prefix)
      # Priority 100 ensures this wins over the PathPrefix router when Host matches
      - "traefik.http.routers.payment-webhook.rule=Host(`hook.bana.nexpando.vn`)"
      - "traefik.http.routers.payment-webhook.priority=100"
      - "traefik.http.routers.payment-webhook.entrypoints=web"
      - "traefik.http.routers.payment-webhook.middlewares=payment-add-prefix@docker,security-headers@file"
      # Middleware — rewrite /v1/api/* to /v1/api/payment/*
      - "traefik.http.middlewares.payment-add-prefix.replacepathregex.regex=^/v1/api/(.*)"
      - "traefik.http.middlewares.payment-add-prefix.replacepathregex.replacement=/v1/api/payment/$$1"
      # Service (shared by both routers)
      - "traefik.http.services.payment.loadbalancer.server.port=3000"
      - "traefik.http.services.payment.loadbalancer.healthcheck.path=/v1/api/payment/health"
      - "traefik.http.services.payment.loadbalancer.healthcheck.interval=30s"

Request flow for 3rd-party webhook:

API Portal (Catch-all, lowest priority)

Source: infrastructure/deployments/develop/gateway/docker-compose.yml

The Portal is an Astro + React dashboard that shows service health, OpenAPI endpoints, response times, and system overview. It runs as an nginx container serving the built static output and catches all unmatched routes.

yaml
services:
  dev-nx-portal:
    image: nginx:1.27-alpine
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.portal.rule=PathPrefix(`/`)"
      - "traefik.http.routers.portal.entrypoints=web"
      - "traefik.http.routers.portal.priority=1"
      - "traefik.http.routers.portal.middlewares=dashboard-auth@file"
      - "traefik.http.services.portal.loadbalancer.server.port=80"

The dashboard-auth@file middleware applies the same HTTP Basic Auth as the Traefik dashboard (user: nx.eventry).

8. Health Check Polling

Traefik actively polls every backend service's health endpoint every 30 seconds. Unhealthy services are removed from routing until they recover. This runs independently from request handling.

9. Auto-Discovery Flow

When a new service container starts with traefik.enable=true, this happens automatically:

No restart of Traefik is required. No central route configuration to update. The service declares its own route and Traefik picks it up within seconds.

10. Middleware Provider Namespacing

Middlewares defined in packages/gateway/config/dynamic/middlewares.yml (file provider) must be referenced with the @file suffix in Docker labels. Without the suffix, Traefik looks for the middleware in the @docker provider and fails silently.

yaml
# Correct — references middleware from file provider
- "traefik.http.routers.commerce.middlewares=rate-limit@file,circuit-breaker@file,security-headers@file"

# Wrong — Traefik looks for "rate-limit" in Docker provider (doesn't exist)
- "traefik.http.routers.commerce.middlewares=rate-limit,circuit-breaker,security-headers"

11. Router Priority & PathPrefix Gotcha

Traefik's PathPrefix is a pure prefix match — it does not enforce a / boundary. This means:

PathPrefix RuleRequest PathMatches?
PathPrefix(/v1/api/payment)/v1/api/payment/webhookYes
PathPrefix(/v1/api/payment)/v1/api/payments/vnpay/ipnYes (no / boundary!)
PathPrefix(/v1/api/sale)/v1/api/sale-ordersYes (same issue)

When multiple routers match the same request, Traefik picks the one with the highest priority. Default priority is calculated from the rule string length, which can lead to unpredictable results.

When to set explicit priority:

  • When a Host-based router overlaps with a PathPrefix router (like payment-webhook vs payment)
  • When a catch-all router (like SPA client with PathPrefix(/)) must have the lowest priority
  • When two PathPrefix rules overlap (e.g., /v1/api/sale vs a hypothetical /v1/api/sale-orders)
yaml
# Higher number = higher priority (wins)
- "traefik.http.routers.payment-webhook.priority=100"   # Wins over PathPrefix routers
- "traefik.http.routers.client.priority=1"               # Loses to everything else

12. Service Base Path Configuration

Each service sets its base path via environment variable:

bash
# infrastructure/deployments/develop/<service>/.env.development
APP_ENV_SERVER_BASE_PATH=/v1/api/<service>
ServiceAPP_ENV_SERVER_BASE_PATH
identity/v1/api/identity
commerce/v1/api/commerce
sale/v1/api/sale
finance/v1/api/finance
inventory/v1/api/inventory
payment/v1/api/payment
signal/v1/api/signal

13. Adding a New Service

  1. Set APP_ENV_SERVER_BASE_PATH=/v1/api/<new-service> in the service's .env.development
  2. Add Traefik labels to the service's docker-compose.yml:
    yaml
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.<new-service>.rule=PathPrefix(`/v1/api/<new-service>`)"
      - "traefik.http.routers.<new-service>.entrypoints=web"
      - "traefik.http.routers.<new-service>.middlewares=rate-limit@file,circuit-breaker@file,security-headers@file"
      - "traefik.http.services.<new-service>.loadbalancer.server.port=3000"
      - "traefik.http.services.<new-service>.loadbalancer.healthcheck.path=/v1/api/<new-service>/health"
      - "traefik.http.services.<new-service>.loadbalancer.healthcheck.interval=30s"
  3. Add the compose file to infrastructure/deployments/develop/dc COMPOSE_FILES array
  4. Traefik auto-discovers the new service — no gateway config changes needed

14. Inter-Service Communication

Services calling each other (e.g., commerce → identity) should NOT go through Traefik. They communicate directly via Docker network:

bash
# Direct container-to-container (does not go through gateway)
APP_ENV_IDENTITY_SERVICE_BASE_URL=http://dev-nx-identity:3000/v1/api/identity
APP_ENV_COMMERCE_SERVICE_BASE_URL=http://dev-nx-commerce:3000/v1/api/commerce

15. K8s Migration Path

Docker labels convert mechanically to Traefik IngressRoute CRDs:

yaml
# Docker label
traefik.http.routers.identity.rule=PathPrefix(`/v1/api/identity`)

# K8s IngressRoute CRD equivalent
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
spec:
  routes:
    - match: PathPrefix(`/v1/api/identity`)
      services:
        - name: identity-service
          port: 3000
DocumentDescription
Gateway OverviewIdentity card + service catalog
ArchitectureC4 views, request-routing flows
MiddlewaresRate limiting, circuit breaker, security headers
ResilienceCircuit breaker states, health checks, retry
ConfigurationLocal Nginx route table, constants
DecisionsADR-0001 (label routing), ADR-0002 (dev parity)

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.