Skip to content

Gateway Specification โ€” App Gateway

Status: Draft ยท Date: 2026-05-28

Implementation contract for the App Gateway โ€” the nginx reverse proxy that is the single public surface in front of the Frontend SPA, the Dashboard.Api, and the Demo Driver.

Sources of truth

Source Owns
docs/SAD.md ยง7 Architecture โ€” gateway as sole public surface, internal upstreams.
docs/api/openapi.yaml API paths the gateway routes (/api/*, /healthz, /readyz).
docs/API_SPECIFICATION.md API CORS / SSE behaviour the gateway pairs with (D3, D6).
docs/DEMO_DRIVER_SPECIFICATION.md Demo driver paths (/demo/*) and SSE stream the gateway proxies.

CR-#### / ADR-#### documents referenced elsewhere do not exist โ€” ignore those citations.


1. Role

Thin nginx reverse proxy โ€” routing + SSE plumbing only: - No auth, business logic, API response caching, rate limiting, or request rewriting beyond path routing. - All state stays in the API. - Config-only image โ€” nginx + a replacement conf template; no second component that knows the contract.


2. Decisions

# Decision Rationale
GW1 Single public surface; fronts three internal-only upstreams (frontend SPA, API, demo-driver). SAD ยง7, NFR-04.
GW2 Config-only image โ€” nginx + a replacement conf template. No unit tests. Config-only by design; behaviour coverage via the integration suite.
GW3 SPA fallback (try_files โ€ฆ /index.html) is owned by the frontend container, not the gateway. Gateway proxies / blindly โ†’ stays contract-agnostic. (G-Q1)
GW4 /healthz + /readyz are proxied to the API. Ops reachability through the single surface. (G-Q2)
GW5 Env-agnostic config via envsubst with a whitelisted var set. One image, per-environment upstreams (local-compose vs Azure). Platform-agnostic (SAD ยง6). (G-Q3)
GW6 Gateway mode is the default; API CORS stays off (CORS_ALLOWED_ORIGINS empty). Single origin โ†’ no CORS. The split-domain CORS path (backend D6) is the gateway-less alternative; the two modes are mutually exclusive.
GW7 Base = nginxinc/nginx-unprivileged, non-root, listens 8080. Matches integration :8080 + a non-root container target.
GW8 Gateway does not terminate TLS; serves plain HTTP on :8080. Internal-only network (NFR-04). TLS, where required, is a hosting concern outside this spec. (G-Q4)
GW9 No build-time nginx -t; no API caching; no rate limiting (reserved, guidelines ยง9). Keep the build + image minimal. (G-Q5)
GW10 Scalar reference UI + OpenAPI document are proxied to the API (/scalar*, /openapi/*). API docs reachable through the single public surface; read-only, consistent with public GET /api/* reads.

3. Solution layout

The image builds from gateway/Dockerfile with build context gateway/.

gateway/
  Dockerfile                 # FROM nginxinc/nginx-unprivileged; COPY template
  default.conf.template      # โ†’ /etc/nginx/templates/default.conf.template (envsubst at start)

The official nginx entrypoint renders *.template from /etc/nginx/templates/ into /etc/nginx/conf.d/ via envsubst on container start.


4. Routing matrix

Path Upstream Treatment
/api/events/stream api Dedicated SSE block โ€” see ยง5
/api/ api JSON read/write; default buffering
/scalar, /scalar/ api Scalar API-reference UI (read-only docs) โ€” bare path redirects to /scalar/v1
/openapi/ api OpenAPI document (/openapi/v1.json), fetched in-browser by Scalar
/demo/stream demo-driver Dedicated SSE block โ€” same settings as /api/events/stream
/demo/control-stream demo-driver Dedicated SSE block โ€” same settings as /api/events/stream
/demo/ demo-driver Demo driver control API + panel; default buffering
/healthz, /readyz api API probes, proxied (GW4)
/health gateway-local return 200 โ€” gateway liveness (integration + platform probe)
/ (all else) frontend SPA assets + Angular routes; frontend owns try_files fallback (GW3)

5. SSE handling (the one critical block)

The stream location must disable every buffering/batching layer or live updates stall and violate NFR-03 (5 s):

location /api/events/stream {
    proxy_pass http://api;
    proxy_http_version 1.1;
    proxy_set_header Connection '';
    proxy_buffering off;          # nginx must not batch the stream
    proxy_cache off;
    gzip off;                     # gzip buffers the stream
    proxy_read_timeout 3600s;     # long-lived connection
    chunked_transfer_encoding on;
}

The API additionally emits X-Accel-Buffering: no (belt-and-braces).

The same block applies verbatim to location /demo/stream and location /demo/control-stream โ€” replace proxy_pass http://api with proxy_pass http://demo-driver.


6. Config template (shape)

upstream frontend    { server ${FRONTEND_UPSTREAM}; }
upstream api         { server ${API_UPSTREAM}; }
upstream demo-driver { server ${DEMO_DRIVER_UPSTREAM}; }

server {
    listen 8080;
    server_tokens off;
    client_max_body_size 256k;        # ingest bodies are tiny

    location = /health { return 200 "ok\n"; access_log off; default_type text/plain; }

    location /api/events/stream { ... }   # ยง5
    location /demo/stream           { ... }   # ยง5 โ€” same SSE block, upstream = demo-driver
    location /demo/control-stream   { ... }   # ยง5 โ€” same SSE block, upstream = demo-driver
    location /api/    { proxy_pass http://api; }
    location /demo/   { proxy_pass http://demo-driver; }
    location /healthz { proxy_pass http://api; }
    location /readyz  { proxy_pass http://api; }
    location = /scalar  { proxy_pass http://api; }
    location /scalar/   { proxy_pass http://api; }
    location /openapi/  { proxy_pass http://api; }
    location /        { proxy_pass http://frontend; }
}

Standard proxy headers (Host, X-Forwarded-For, X-Forwarded-Proto) set on the proxied locations.


7. Configuration (env)

Var Example Purpose
FRONTEND_UPSTREAM frontend:8080 frontend upstream host:port
API_UPSTREAM api:8080 API upstream host:port
DEMO_DRIVER_UPSTREAM demo-driver:3001 demo driver upstream host:port
NGINX_ENVSUBST_FILTER ^(FRONTEND_UPSTREAM\|API_UPSTREAM\|DEMO_DRIVER_UPSTREAM)$ restrict envsubst to our vars so nginx's own $host/$uri survive

Examples are illustrative; actual upstream host:port come from the frontend / API deployment specs per environment.


8. Testing

Config-only โ‡’ no unit tests. Behaviour is verified by the cross-stack integration suite, which drives real traffic โ€” routing, SSE passthrough, health โ€” through the running gateway at http://localhost:8080.


9. Out of scope

  • Auth (the API owns X-Api-Key).
  • Rate limiting (reserved; guidelines ยง9).
  • API response caching (NFR-05 statelessness โ€” every read hits the DB).
  • SPA-fallback logic (frontend container owns it).
  • TLS termination (internal-only; hosting concern).
  • Request rewriting beyond path routing.