Provision a protected, Foundry-backed Function App proxy for SPFx web parts. One CLI, no config required, one resource group.
spfx-foundry-deploy provisions everything an SPFx web part needs to talk to Microsoft Foundry without putting a single secret in the browser bundle:
- Resource Group + AI Foundry resource + a
gpt-5-family model deployment - Storage Account + Function App (Node 22, Linux, Consumption)
- Backend API Entra app +
user_impersonationscope + SPFx tenant-wide grant (soAadTokenProvider.getToken("api://<backend-app-id>")just works) - Managed identity to Foundry (
Cognitive Services OpenAI Userrole) - Easy Auth in Entra-required, return-401 mode (audience-pinned to the Backend API)
- Platform hardening: HTTPS-only, TLS 1.2, FTP disabled
- Application Insights wired
Then it patches the calling web part's config/serve.json with the
endpoint URL and Backend API resource so npm start opens the workbench
ready to chat.
No function key in the browser. The Entra token from AadTokenProvider
is the only auth gate. See Security posture for details.
Connecting SPFx → Azure OpenAI / Microsoft Foundry the right way needs a half-dozen Azure resources, a tricky Entra app + scope dance, and an Easy Auth configuration that's easy to get subtly wrong. Most samples shortcut this with a function key embedded in the SPFx property pane — which is a shared secret in a public client.
This tool ships an opinionated, secure-by-default version of that provisioning so you can focus on the web part.
cd webparts/my-webpart
npx -y github:ferrarirosso/spfx-foundry-deploy deployThat's it. No JSON to write — the deployer reads slug from your
package.json name field and defaults profile to
chat-completions. The form walks you through a one-screen review
(prefix, region, model — all interactive) and confirms. Five-or-so
minutes later you have a running, hardened proxy and a serve.json
that's ready for npm start.
Same shape if you want it pinned in your dev dependencies:
npm install --save-dev @ferrarirosso/spfx-foundry-deploy
npx spfx-foundry-deploy deployDrop one next to your webpart only if you want to commit defaults or pre-declare values the deployer should prompt for at deploy time. Everything is optional — the file is a partial override of what the form already asks:
serveProperties keys with empty-string values are the contract for
"ask me at deploy time" — useful for MCP webparts that need a Power
Platform Environment ID GUID per tenant. The deployer prompts for each
empty value and writes the answer into the patched serve.json only;
your tracked deploy.config.json stays clean.
Resource Group
├── AI Foundry account (kind: AIServices)
│ └── gpt-5-mini deployment (GlobalStandard, default capacity)
├── Storage Account (Standard_LRS)
├── Function App (Node 22, Linux, Consumption)
│ ├── System-assigned managed identity
│ │ └── role: Cognitive Services OpenAI User on the AI Foundry resource
│ ├── Easy Auth: require auth, audience = api://<backend-app-id>, return 401
│ └── App Insights connection string (telemetry on)
└── (Resource group also indirectly tracks the Backend API Entra app —
the Entra app lives in your tenant, not in the RG, so teardown
offers to delete it separately.)
spfx-foundry-deploy deploy # provision + wire serve.json
spfx-foundry-deploy setup # re-wire serve.json from .deploy-output.json
spfx-foundry-deploy teardown # delete RG + purge soft-delete + delete Entra app
spfx-foundry-deploy setup-local # generate backend/local.settings.json for `func start`Common flags:
| Flag | Subcommand | Effect |
|---|---|---|
--config <path> |
all | Optional. Path to a deploy.config.json. When omitted, slug is inferred from package.json and profile defaults to chat-completions. |
--dry-run |
deploy |
Walk through the form, print the plan, no Azure calls. |
--no-wire |
deploy |
Skip the serve.json patch. |
--keep-app |
teardown |
Don't delete the Backend API Entra app. |
--no-purge |
teardown |
Skip the AI Services soft-delete purge. |
--keep-rg |
teardown |
Don't delete the resource group. |
--yes |
teardown |
Skip the type-the-name confirmation. CI use only. |
Every field is optional. Full schema: deploy.config.schema.json.
| Field | Effect |
|---|---|
slug |
Stable identifier — keys .deploy-output.json so multiple webparts in one repo can coexist. Defaults to your package.json name field (with @scope/ stripped). |
profile |
Provisioning recipe. Currently always chat-completions. Defaults to chat-completions. |
namePrefix |
Drives every Azure resource name. The deploy form lets you regenerate or edit at run time. Defaults to slug. |
location |
Default region. The form lets you change it; the model picker re-validates against the new region. Defaults to swedencentral. |
model.name |
Default model. The form's region-scoped picker shows what's actually available. Defaults to gpt-5-mini. |
requestLimits.perMinute / perDay |
Per-caller rate limits enforced by the proxy. Defaults: 30/min, 1000/day. |
serveProperties |
Extra string properties merged into the patched serve.json. Empty-string values are prompted for at deploy time (useful for per-tenant GUIDs like an MCP environmentId). |
The browser never sees a shared secret. Auth chain in one line:
SPFx → AAD bearer for api://<backend-app> → Easy Auth (audience-pinned, Entra-required, returns 401) → app-role check (returns 403 if the user doesn't have <namePrefix>.User) → Function code → managed identity → Foundry. No function key, no API key in production.
In bullets:
- Authentication: Easy Auth (
requireAuthentication: true,Return401), audience-pinned to the Backend API. - Authorization: deployer creates one app role per deployment (
<namePrefix>.User); proxy returns 403 if missing. - Default-deny in production: runtime throws at startup if
WEBSITE_INSTANCE_IDis set andREQUIRED_APP_ROLEis empty (unlessALLOW_ANONYMOUS_AUTHZ=trueis the explicit opt-out). - Keyless to Foundry: managed identity only; throws if
AZURE_OPENAI_API_KEYis set on a deployed Function App. - CORS: locked down to your SharePoint tenant origin (App Service CORS layer + in-app validation).
- Platform: HTTPS-only, TLS 1.2, FTP off, App Insights.
- Diagnostics:
/healthis{ status, time }. Chat responses surface only a correlation id and rate-limit hints — no caller id, deployment name, or auth-method leaks.
Manual admin steps (one-time per tenant):
- Approve the SPFx → Backend API grant (deployer auto-grants via Graph; manual fallback URL printed if it can't).
- Assign the
<namePrefix>.Userrole to users (the deployer auto-assigns the deploying user; print URL for onboarding additional users). - Approve any
webApiPermissionRequestsfrom the .sppkg in SharePoint Admin Center.
Full breakdown in CHANGELOG.md.
Written at the consuming repo's root, keyed by slug. Gitignored. No secrets — Easy Auth handles browser auth, so the only "credentials" are the bearer tokens SPFx acquires at runtime.
{
"my-webpart": {
"backendUrl": "https://my-webpart-proxy.azurewebsites.net/api",
"backendApiResource": "api://<backend-app-id>",
"backendApiAppId": "<backend-app-id>",
"backendApiAppDisplayName": "my-webpart Backend API",
"namePrefix": "my-webpart",
"resourceGroup": "rg-my-webpart",
"location": "swedencentral",
"aiServicesName": "my-webpart-ai",
"functionAppName": "my-webpart-proxy",
"modelName": "gpt-5-mini",
"deploymentName": "my-webpart-gpt5mini",
"profile": "chat-completions",
"deployedAt": "2026-04-26T10:00:00.000Z"
}
}The deployer also patches <webpart>/config/serve.json with
backendUrl and backendApiResource under
serveConfigurations.default.webPart.properties, so npm start opens the
SharePoint workbench with the property pane already filled in.
For non-SPFx consumers (or anything that needs different wiring): the
deployer's job ends at writing .deploy-output.json. Add your own
setup.mjs that reads that JSON and writes whatever shape your project
needs (.env.local, generated TypeScript, etc.).
The proxy enforces authorization via Entra app role assignment. The
deployer creates one app role on the Backend API registration with value
<namePrefix>.User. The Function App's REQUIRED_APP_ROLE env var is set
to the same value at deploy time. The proxy returns 403 to authenticated
callers who don't have the role assigned.
To grant a user access:
- Go to Entra ID → Enterprise applications →
<namePrefix> Backend API. - Users and groups → Add user/group.
- Select the user (or a group, if your tenant has Entra ID P1+).
- Pick the
<namePrefix> backend userrole and assign.
The deploy command prints the exact Portal URL at the end so you can jump straight to step 1.
Default-deny: in a deployed Function App, REQUIRED_APP_ROLE MUST
be set or the function refuses to start. The deployer always sets it;
admins who explicitly want an open backend can set
ALLOW_ANONYMOUS_AUTHZ=true as an app setting (not recommended outside
internal experiments).
App role assignment is the default because it's app-scoped (the role exists only on this Backend API). For tenants that prefer to manage access via security groups instead, the equivalent setup is documented but not built into the deployer:
- Create or pick an Entra security group; copy its Object ID.
- In the Backend API app manifest, set
groupMembershipClaims: "SecurityGroup"so the user's group memberships land in the token. (One-line manifest patch viaaz rest.) - Add a backend env var
ALLOWED_GROUP_IDS=<comma-separated-guids>and replace the role check in the proxy with a group check (parsegroupsclaim fromx-ms-client-principal).
This swap is the kind of change you'd do once, in a fork, if your tenant governance is group-centric. The default app-role flow is what this deployer ships.
- Node ≥ 22
- Azure CLI ≥ 2.69
- Azure Functions Core Tools v4 (
func) - An Azure subscription with quota for
gpt-5-mini(or your chosen model) in the target region - A Microsoft 365 tenant with SharePoint Online (the SPFx grant targets the SharePoint Online first-party app)
{ "$schema": "https://raw.githubusercontent.com/ferrarirosso/spfx-foundry-deploy/main/deploy.config.schema.json", "namePrefix": "my-webpart", "location": "swedencentral", "model": { "name": "gpt-5-mini" }, "serveProperties": { "environmentId": "" } }