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:
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
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/…
- Destinations. A cluster that belongs to an environment carries metadata
EnvironmentDestination: <logical cluster>. The gateway adds each environment's backend to it as a destinationenv-{name}, taggedEnvironment: {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-scopedunder/env/…; call them without the prefix. - Headers. The gateway sets
X-CC-Environmentand drops any the caller sent. YARP setsX-Forwarded-Prefixfrom the prefix and replaces a caller's. - At the backend (
Shared.Web.EnvironmentRequests, WebApi and the main API): a request whoseX-CC-Environmentis not the host's environment answers 421 Misdirected Request, never data from the wrong database. The prefix becomesPathBaseonly when it is exactly/env/{X-CC-Environment}, so URLs built withLinkGenerator(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
GET /bff/environmentslists the catalogue as the SPA needs it: name, display name, order,isProduction, andallowed(OPAenvironment.accessfor 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 frommain.tsxbefore the router exists) and becomes the router'sbasename, so every link andnavigate('/…')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 (localStoragecc.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 streamingfetchgo throughenvironmentUrl, 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
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.