Deploying to Azure App Service

A Power Portals Pro portal is a single ASP.NET Core application whichever stack you chose, so deploying it is an ordinary dotnet publish to an ordinary App Service. This page covers the resources to create, the settings the portal reads, what we suggest as defaults, and the handful of steps that are specific to Power Portals Pro rather than to ASP.NET Core.

What you are deploying

Both templates produce one web application and one publish output. There is no separate front-end host to stand up, no static-site resource, and no second App Service:

Note

Because the React publish shells out to npm, Node has to be installed on whatever machine runs dotnet publish — your laptop, or the build agent. The template's client asks for Node ^20.19.0 || >=22.12.0. App Service itself never needs Node; it only ever sees the built output.

Suggested defaults

Starting points, not rules — but these are what we would pick for a new production portal.

1. Create the App Service

Three commands. Substitute your own names — myportal is used throughout this page.

The web app name becomes https://myportal.azurewebsites.net and has to be globally unique. Choose it deliberately: unless you put a custom domain in front, it is the URL your licence key gets bound to.

2. Set the platform options

These are the ones whose defaults are wrong for a portal.

3. Configuration

App Service application settings arrive as environment variables and override appsettings.json, which is exactly what you want: leave the file alone and set the per-environment values here.

Important

User secrets do not travel. Everything you stored with dotnet user-secrets during development lives only on your machine. Every one of those keys has to be recreated as an application setting — or a Key Vault reference — or the deployed app will not start.

Use a double underscore for the section separator: D365:ClientId becomes D365__ClientId. The colon form works on Windows App Service but not on Linux, so __ is the form to use everywhere.

Always required

Required if you enabled that sign-in provider

Optional

Note

The required keys are read with GetRequiredValue, so a missing one throws during startup rather than failing later at the first Dataverse call. On App Service that shows up as a site that never comes up — read the log stream, and the exception names the key.

4. Give the portal an address to send from

D365__EmailSenderEmailAddress is what account-confirmation and password-reset mail comes from, and the framework resolves it by looking for a Dataverse user with that address, then a queue, then a team. Use a queue. Its address needs a shared mailbox in Exchange Online, which takes no licence, and everything else happens in Dataverse. Pointing the setting at a real person instead costs you three things:

Four steps: one in Exchange, then three in Dataverse. The MCP server does the three Dataverse steps in one call with create_email_queue, and check_email_sender says which step is missing when mail does not arrive:

  1. Create a shared mailbox with the address, in the Exchange admin centre (Recipients → Mailboxes → Add a shared mailbox). It needs no licence. It is what server-side synchronisation sends through, so without it the last step fails with an error about validating the mailbox configuration in the remote server.
  2. Create a private queue with the address (e.g. noreply@contoso.com), incoming delivery None and outgoing delivery Server-Side Synchronization. The queue’s name is what recipients see as the sender, so name it for the portal rather than for a person.
  3. Approve the address. Two flags have to line up and they live on different records: the queue’s own email-address approval, and the mailbox’s approved by O365 admin. A queue is created pending on both, and only an Office 365 Global Administrator or Exchange Administrator may approve the second.
  4. Test & Enable the mailbox under Power Platform admin centre → Settings → Email configuration → Mailboxes. It sends a test message from the address to itself, and the mailbox only sends once it has succeeded. create_email_queue runs it and reports the result.

Important

An unapproved or untested mailbox fails silently. Dataverse accepts the email, records it, and never delivers it — which from the portal’s side is indistinguishable from a successful send. Nothing throws, nothing is logged as an error, and the first sign of trouble is a user saying they never got their confirmation link. If registration emails are not arriving, check the mailbox’s outgoing status before anything else: Not Run means the test never ran, and Failure with an alert about validating the mailbox configuration in the remote server means the address has no shared mailbox in Exchange Online. check_email_sender reports both, with the mailbox’s alerts.

5. Keep the secrets in Key Vault

Give the app a managed identity, grant it read access to a vault, and put each secret's URI in the app setting instead of the secret itself. App Service resolves the reference for you, and the value never appears in the configuration blade, in a script, or in a deployment log.

When a client secret expires — Entra defaults to a year — add a new version to the vault and restart the app. The app setting itself does not change.

The MCP server does this whole sequence: create_managed_identity makes a user-assigned identity, enable_managed_identity attaches it to the app, create_key_vault creates the vault and grants that identity read access, and the secret tools then accept destination: "key-vault" — which sends a freshly minted client secret from Entra straight into the vault and leaves only the reference in the app settings. The value never becomes a literal setting and never passes through anybody’s hands. Secrets already in your user secrets go with push_secrets_to_key_vault, which asks you to tick each one before it is copied and points the matching app settings at them; check_deployment then confirms every reference’s secret exists and that the app’s identity may read it.

Important

A Key Vault name is reserved for 90 days after deletion. Soft delete is on by default and cannot be turned off, so deleting a vault does not release its name — recreating one with the same name inside that window needs an explicit purge, which itself needs permission and is irreversible. It is the one resource here that is not cheaply undoable, so choose the name as deliberately as you would the App Service’s.

Note

Better still, drop the Dataverse client secret altogether. Set D365__ManagedIdentityId to a managed identity’s client id and the portal authenticates to Dataverse as that identity, with no secret and therefore nothing to expire. There is no environment check and no build switch: the setting belongs in the App Service’s configuration rather than appsettings.json, so it is absent locally and present when deployed, and one build behaves correctly in both. Use a user-assigned identity — a system-assigned one is created and destroyed with the app and differs per deployment slot, so rebuilding the app mints a new identity and orphans the Dataverse application user built on the old one. The identity still needs that application user, created from its application id rather than the principal id the Azure portal shows first. This applies only when the App Service and the Dataverse environment are in the same Entra tenant; if they are not, see When Dataverse is in another tenant below.

When Dataverse is in another tenant

Everything above assumes the App Service and the Dataverse environment belong to the same Entra tenant — the usual case when an organisation hosts its own portal. If you run the portal and your customer owns the Dataverse environment, they are in different tenants, and the identity advice changes.

Important

A managed identity cannot authenticate to Dataverse in another tenant on its own. Its service principal belongs to the subscription’s own tenant and cannot be consented into anybody else’s, so there is nothing for the customer’s Dataverse to create an application user against. Do not set D365__ManagedIdentityId by itself in this shape — the portal would get a token and then be refused by every call, which reads like a broken security role rather than the wrong kind of identity. It can, though, be the credential of a multi-tenant app registration, which is what crosses tenants: see the first option below.

The portal signs in as an app registration that the customer’s tenant has a service principal for. There are three ways to arrange that, and they differ in who controls the credential’s lifetime:

Whichever you choose, an administrator in the customer’s tenant has to consent to the registration once. That is what creates the service principal there — an object carrying your client id, which their Dataverse can then bind an application user to. Send them this link:

Important

Consent comes before the application user, not after. Until it has happened there is no service principal in the customer’s tenant, so creating the Dataverse application user fails with an error about an unknown application — which mentions neither tenants nor consent, and is the single most confusing point on this path. You cannot perform this step for them: it is their tenant granting access to a registration they do not own, so it needs their authority rather than yours.

Put that secret in Key Vault rather than in an application setting. It is a credential against somebody else’s data, and a literal setting is readable by anyone with Contributor on the app and travels with any export of the configuration. check_deployment reports it if you do not.

6. Publish

Publish framework-dependent, zip the output, push the zip. On a React portal the same dotnet publish also builds the SPA and stages it into wwwroot.

Zip the contents of the publish folder, not the folder itself — App Service unpacks the archive straight into the site root, so a nested top-level directory produces a site that serves nothing.

Visual Studio's Publish dialog does the same thing interactively, and a GitHub Actions or Azure Pipelines workflow does it on push. The only extra step for a React portal is a Node setup step ahead of the build, since dotnet publish shells out to npm.

7. Point the licence at the deployed URL

The website licence is bound to a URL. In the Power Portals Pro model-driven app, open (or create) the Portal Website record holding your licence key and set its URL to the production URL of the portal you just deployed — https://myportal.azurewebsites.net, or your custom domain if one is going in front of it.

Register the production URL only. Validation also accepts -dev, -test, -uat, -stage and -prod suffixed hosts, and the same five as leading subdomains, so other environments need no separate record. The Licensing page has the full list.

Important

Do this before you send traffic. Every data-serving endpoint checks the licence, so until the record exists and matches the host, the portal renders but every data call comes back 402.

8. Add the production redirect URI

If the portal offers Microsoft sign-in, the sign-in app registration still only knows your localhost redirect URI. Add the deployed one alongside it — Entra accepts several, so the local one keeps working:

Same pattern for the other providers on their own callback paths (/signin-google, /signin-facebook). Add one per environment, and one per custom domain.

Deployment slots

A staging slot and a swap give you a warmed-up app and an instant rollback. They are the one thing genuinely worth leaving Basic for — and the reason to leave it when you need them, rather than in advance. Two things need care.

Data Protection keys are per-slot. ASP.NET Core persists the key ring under %HOME%, which each slot has its own copy of, so a swap replaces it — and every authentication cookie issued by the previous slot becomes undecryptable. Everyone is signed out. Move the key ring somewhere both slots share before you rely on swapping:

Mark the environment-specific settings as slot settings so they stay behind during a swap. The Dataverse environment URL is the one that bites: without it, swapping a staging slot that points at a sandbox promotes that sandbox connection into production.

Scaling out

Scaling out is a decision to make when you measure the need for it, not when you provision. One instance of the default tier carries a surprising amount of portal traffic.

Doing all of this from an AI agent

The Power Portals Pro MCP server can perform every step on this page. Bind the folder to a subscription once, at a terminal — the binding belongs to that folder, so several projects can target different customers at the same time, and nothing machine-wide changes:

Note

Every tool that changes anything in Azure shows you what it will do before you are asked to confirm, including what it will cost — the real monthly figure for your region, with a note that retail list ignores any Enterprise Agreement or reservation you hold. Tools that only read never prompt. The server does not use the Azure CLI and never reads or changes its signed-in state.

Troubleshooting