Skip to content

Architecture at a glance

The plain-English version of this page is Why FoundryGate?; the narrative walkthrough with the Entra sync and quota-resolution detail is the System Overview.

The whole system in one picture: a developer’s coding tool talks to one gate, the gate decides yes or no before anything reaches a model, and a separate app — never on the request path — decides who gets a key and how big their budget is.

Read it top to bottom. Every box links to the reference page for that part.

FoundryGate architecture — generated from infra/ and src/

FoundryGate architecture, generated from infrastructure and source code. Developer CLIs (Claude Code sending x-api-key, Codex CLI sending api-key) call the API Management gateway. The gateway has 2 front doors: /anthropic and /openai/v1. Each developer key belongs to one of 3 tier products: Standard, 5M tokens per month, 20K tokens per minute, permitting gpt, haiku, sonnet; Power, 20M tokens per month, 40K tokens per minute, permitting gpt, haiku, sonnet; Unlimited, no monthly cap, 100K tokens per minute, permitting gpt, haiku, opus, sonnet. Requests route through backend pools to Azure AI Foundry accounts in eastus2 (primary, 4 model deployments) and swedencentral (pool-member, 3 model deployments). Out of the request path, the control plane runs the Blazor UI, the API on Container Apps, Azure SQL, Key Vault and 3 timer functions: EntraSyncFunction daily at 02:00 UTC, MonthlyQuotaResetFunction daily at 00:01 UTC, UsageSyncFunction every 15 minutes.

1 · What developers run their laptop, their own key
HTTPS — one personal APIM subscription key per developer
2 · The gate Azure API Management — every limit is enforced here, on the request itself

Front doors — one per model provider

fg-require-product — a key with no tier product is refused

Tier products — the developer's key belongs to exactly one

llm-token-limit + the alias allowlist, counted against the developer's own subscription
200 · headers carry what's left 429 + Retry-After · going too fast 403 · monthly budget spent, or model not on the tier
gateway managed identity — no Foundry account keys exist anywhere

Backend pools — where an allowed request is sent

foundry-anthropic-pool
priority 1 eastus2 → priority 2 swedencentral · circuit breaker per backend, retried across the pool
foundry-openai (primary account)
eastus2 only
3 · The models Azure AI Foundry accounts, one per region in foundryRegions
Out of band — the gateway streams per-request token logs to Log Analytics; the control plane reads them afterwards. It is never consulted during a request.
4 · The app that manages it deployed only when deployControlPlane is true
How it gets built the Bicep module graph of infra/main.bicep — an arrow means "consumes that module's outputs"
  1. monitoring no dependencies modules/monitoring.bicep
  2. foundry per region no dependencies modules/foundry.bicep
  3. apim ← monitoring modules/apim.bicep
  4. foundryRbac per region ← apim, foundry modules/foundry-rbac.bicep
  5. gateway ← apim, foundry, foundryRbac modules/ai-gateway.bicep
  6. controlPlane if deployControlPlane ← apim, foundry, gateway, monitoring modules/control-plane.bicep
  7. swaPreviewRole if deployControlPlane && deploySwaPreviewRole ← controlPlane modules/swa-preview-role.bicep

The control plane module fans out into 9 more: identities, keyVault, registry, storage, sql, staticWebApp, rbac, containerApp, functionApp.

And the .NET side — 7 shipping projects, in reference order (each row may only reference the rows above it)

  1. FoundryGate.Domain references nothing
  2. FoundryGate.Data → Domain FoundryGate.Web → Domain
  3. FoundryGate.Cli → Data, Domain FoundryGate.Core → Data, Domain
  4. FoundryGate.Api → Core, Data, Domain FoundryGate.Functions → Core, Data, Domain
the request path — real-time enforcement out of band — telemetry and reconciliation control plane — lifecycle and tiers Generated from infra/ and src/ at build time · commit c996615 · generator

Why this picture can’t go stale

Everything above — the regions, the tier numbers, the model deployment names, the Function schedules, the module dependency arrows — is read out of the repository at build time by docs-site/scripts/generate-architecture.mjs and written to docs-site/src/generated/architecture.json. The page renders that file and nothing else.

Where the picture comes fromWhat it reads
infra/main.bicepfoundryRegions, quotaTiers, productModelAliases, pooledModelDeployments, primaryOnlyModelDeployments, the module declarations and their deployControlPlane conditions, and the fg-role tags
infra/modules/*.bicepthe module dependency edges (which module consumes which module’s outputs), the gateway’s front-door paths and backend pool names, the control plane’s own sub-modules
src/*/*.csprojthe .NET ProjectReference graph
src/FoundryGate.Functions/**every [TimerTrigger] and its real NCRONTAB schedule
src/FoundryGate.Api/**the controller route prefixes

The generator runs as prebuild, so npm run build always regenerates it, and CI regenerates it too and fails the build on git diff --exit-code docs-site/src/generated. Changing a region or a tier without updating the diagram is not something a pull request can quietly do. A Mermaid twin of the module graph is emitted alongside the JSON as architecture.mmd for anything that wants the graph as data.

The three things worth taking away

  1. Enforcement happens on the request, at the gate. The llm-token-limit policy in each tier product decides 429 (too fast) or 403 (monthly budget spent, or model not on this tier), counted against the developer’s own APIM subscription. No database is consulted.
  2. The model allowlist is the alias map. A tier permits exactly the aliases listed on its card — anything else comes back 403 model_not_permitted, with the deployments rotating underneath the alias without a policy redeploy.
  3. The control plane can be absent. deployControlPlane=false — the default — gives you the gateway and the models on their own, which is what a fork should stand up first. The dashed boxes in the module graph are the parts that condition on it.

Live evidence

Responses captured from a real deployed gateway — 429s, 403s, header measurements — are recorded in validation/ in the repository, one file per gateway cycle.