Skip to content

Environments#

One gateway and one SPA manage every CommandCenter environment. Each environment keeps its own backends (WebApi, the main API, the legacy API, BMO) and its own database. A request names its environment in the URL, and the gateway sends it to that environment's backends:

/env/{environment}/api/…   →  that environment's backends
/api/…                     →  the default backends, as before

The backends stay unaware of routing: each serves one environment, and the gateway only sends it requests for that environment.

In the SPA the environment is the first segment of every page's URL, /{environment}/customers, and every API call of that page goes to /env/{environment}/…. When the catalogue is empty, the SPA's URLs and calls stay as they were (/customers, /api/…).

The pieces#

flowchart LR
    Browser["Browser (SPA)"]
    subgraph gateway ["Gateway (CommandCenter.Bff.Frontdoor)"]
        Prefix["/env/{name} → PathBase<br/>404 when not in the catalogue"]
        Rewrite["EndpointRewrite + routes<br/>(unchanged)"]
        Step["environment.access (OPA)<br/>pick the environment's destination"]
    end
    Catalogue["Platform:Environments<br/>(App Configuration, label platform)"]
    OPA["OPA"]
    subgraph prod ["Environment prod"]
        WebApiP["WebApi"]
        CcP["Main API"]
    end
    subgraph uat ["Environment uat"]
        WebApiU["WebApi"]
        CcU["Main API"]
    end

    Browser -- "/env/prod/api/…" --> Prefix --> Rewrite --> Step
    Catalogue -. "read live" .-> Prefix
    Catalogue -. "tagged destinations" .-> Step
    Step -- "environment.access" --> OPA
    Step -- "X-CC-Environment: prod<br/>X-Forwarded-Prefix: /env/prod" --> WebApiP
    Step --> CcP
    Step -.-> WebApiU
    Step -.-> CcU
Hold "Alt" / "Option" to enable pan & zoom

The catalogue#

Each environment is one entry under Platform:Environments:{name}. In App Configuration it lives at label platform, and the gateway reads it again on every refresh, so a new environment is served without a restart. Locally the entry dev is in the gateway's appsettings.Development.json.

"Platform": {
  "Environments": {
    "prod": {
      "DisplayName": "Production",
      "IsProduction": true,
      "Order": 1,
      "Destinations": {
        "next": "https://webapi-prod.internal/",
        "commandcenter": "https://api-prod.internal/",
        "legacy": "https://legacy-prod.internal/CommandCenter/",
        "bmo": "https://bmo-prod.internal:8090/"
      }
    }
  }
}
Field
name (the key) 1–32 lower-case letters, digits and hyphens, not a reserved path (see In the SPA). It is the URL segment and must equal the backends' Configuration:Environment (their App Configuration label).
DisplayName, Order What the SPA shows, and where it sorts.
IsProduction The policy may limit production to some roles (production_roles in the OPA data).
Destinations The environment's backend per logical cluster: next (WebApi), commandcenter (main API), legacy, bmo. Leave one out and that part answers 503 environment-not-served for the environment.

An entry with an invalid name or address is left out and logged (gateway EventIds 10 and 11).

What happens to a request#

sequenceDiagram
    participant SPA
    participant GW as Gateway
    participant OPA
    participant API as WebApi (prod)
    SPA->>GW: GET /env/prod/api/features, X-CSRF: 1
    GW->>GW: prod in the catalogue? (else 404 unknown-environment)
    GW->>GW: PathBase /env/prod, Path /api/features → rewrite → route api-next
    GW->>GW: X-CSRF check
    GW->>OPA: environment.access, prod, isProduction (remembered 30 s when allowed)
    OPA-->>GW: allow (else 401 / 403 / 503)
    GW->>GW: keep only the destination tagged Environment=prod
    GW->>API: GET /api/features, X-CC-Environment: prod, X-Forwarded-Prefix: /env/prod, user token with env=prod
    API->>API: prod is my environment (else 421), PathBase /env/prod
    API-->>SPA: 200, links under /env/prod/…
Hold "Alt" / "Option" to enable pan & zoom
  • Destinations. A cluster that belongs to an environment carries metadata EnvironmentDestination: <logical cluster>. The gateway adds each environment's backend to it as a destination env-{name}, tagged Environment: {name}. A request for an environment uses only that destination. A request without a prefix uses only the untagged ones, so it can never reach an environment's backend.
  • Clusters that are not per environment (chat, system stats, the SPA's own files) answer 404 not-environment-scoped under /env/…; call them without the prefix.
  • Headers. The gateway sets X-CC-Environment and drops any the caller sent. YARP sets X-Forwarded-Prefix from the prefix and replaces a caller's.
  • At the backend (Shared.Web.EnvironmentRequests, WebApi and the main API): a request whose X-CC-Environment is not the host's environment answers 421 Misdirected Request, never data from the wrong database. The prefix becomes PathBase only when it is exactly /env/{X-CC-Environment}, so URLs built with LinkGenerator (task status, downloads) keep working through the gateway. A request without the header (health checks, the bmo CLI, the gateway's un-prefixed routes) is served as before.

In the SPA#

flowchart TD
    Load([page load]) --> List["GET /bff/environments<br/>names, isProduction, allowed"]
    List --> Empty{catalogue empty?}
    Empty -- yes --> Plain["URLs and calls as before<br/>/customers, /api/…"]
    Empty -- no --> First{first URL segment<br/>an environment?}
    First -- yes --> Serve["router basename /{environment}<br/>every API call /env/{environment}/…"]
    First -- no --> Redirect["replace with /{last used, or first allowed}/same page"]
    Serve --> Switch["environment switch in the header"]
    Switch -- "full page load" --> Load
Hold "Alt" / "Option" to enable pan & zoom
  • GET /bff/environments lists the catalogue as the SPA needs it: name, display name, order, isProduction, and allowed (OPA environment.access for the signed-in user). Never the backends. It is listed when signed out too, with nothing allowed, so a deep link keeps its environment through the sign-in.
  • The environment is chosen once per page load (src/services/environment.ts, called from main.tsx before the router exists) and becomes the router's basename, so every link and navigate('/…') in the app stays under it without naming it. A URL that names no environment (an old bookmark, /) is replaced by the same page under the last environment used in this browser (localStorage cc.environment), else the first one the user may use.
  • Switching is a full page load at the same page. No cached query, open stream or half-filled form of one environment can appear in another.
  • API calls: every axios instance (the legacy client, the typed clients, the BMO client, and bare axios) and every streaming fetch go through environmentUrl, which turns /api/…, /v1/… and /v2/… into /env/{environment}/…. Chat, system stats, diagnostics and keyplex (/api/chat, /api/system-stats, /api/d, /api/keyplex) and /bff/… are the same for every environment and are never prefixed.
  • Production is marked in words next to the switch, not only by colour.
  • Names the SPA cannot use are refused by the catalogue (EventId 10): api, assets, bff, env, health, platform, src, static, v1, v2. Do not name an environment after a page either (customers, config): its URL would be read as that environment.

Adding an environment#

journey
    title An operator adds the environment "uat"
    section Deploy
      Install uat's backends with Configuration__Environment=uat: 3: Operator
      Give uat's identities their Azure roles: 3: Operator
    section Register
      Add Platform:Environments:uat (label platform): 4: Operator
      Bump Settings:PlatformSentinel: 4: Operator
    section Use
      Gateway serves /env/uat/… within a refresh: 5: Gateway
      A request for uat reaching a prod host answers 421: 5: Backend
Hold "Alt" / "Option" to enable pan & zoom

Until the /config/environments page exists, the entry is added in App Configuration directly. When the backends answer 421, check that the catalogue points uat at uat's hosts and that those hosts run with Configuration__Environment=uat.