Note
Access to this page requires authorization. You can try signing in or .
Access to this page requires authorization. You can try .
Create and deploy a single-page application in Power Pages
Power Pages supports integrating single-page application (SPA) code created with next-generation AI-assisted tools, like GitHub Copilot. This capability lets developers bring modern, component-based front-end experiences into Power Pages by using natural language as a coding interface.
By guiding, testing, and refining AI-generated code, makers can shift their focus from repetitive implementation tasks to higher-level orchestration. This empowers more intuitive, creative development while maintaining enterprise-grade quality and standards.
This article shows you how to:
- Create and set up an SPA project for Power Pages by using the Power Platform CLI (PAC CLI).
- Upload and download code assets to and from your Power Pages site.
- Set up a secure and maintainable project structure.
- Learn key differences between SPA-based and traditional Power Pages implementations.
Note
- An SPA site is a Power Pages site that runs entirely in the user's browser (client-side rendering). Unlike traditional Power Pages sites, you manage SPA sites only through source code and command-line interface (CLI) tools.
- Power Platform Git integration isn't supported for Single-Page Application (SPA) websites in Power Pages.
Prerequisites
Before you begin, make sure you have:
- A Power Pages environment with admin privileges.
- Power Platform CLI (PAC CLI) version 1.44.x or later installed and authenticated.
- A Power Pages site on version 9.7.4.x or later.
- Allow JavaScript file uploads in Dataverse environments.
- A local Git repository with your custom front-end project, such as React, Angular, or Vue.
Allow JavaScript file uploads
By default, some Dataverse environments block the upload of JavaScript (.js) files. If you encounter the error "Import failed: The attachment is either not a valid type or is too large. It cannot be uploaded or downloaded.", update your environment settings to allow this file type.
To adjust the settings in the Power Platform admin center for an environment, follow these steps:
- Sign in to the Power Platform admin center.
- In the navigation pane, select Manage.
- In the Manage pane, select Environments.
- Select an environment.
- In the command bar, select Settings.
- Expand Product, and then select Privacy + Security.
- In the Blocked Attachments section, remove
jsfrom the list of file extensions. - Select Save.
Create and deploy an SPA site
Power Pages SPA sites are managed using the PAC CLI commands upload-code-site and download-code-site. After you upload a site, it appears in Power Pages in the Inactive sites list. Activate the site to make it available to users.
Upload an SPA site
Use the pac pages upload-code-site command to upload your local source and compiled assets to your Power Pages environment.
Syntax
pac pages upload-code-site `
--rootPath <local-source-folder> `
[--compiledPath <build-output-folder>] `
[--siteName <site-display-name>]
Parameters
| Parameter | Alias | Required | Description |
|---|---|---|---|
--rootPath |
-rp |
Yes | Local folder that has your site's source files |
--compiledPath |
-cp |
No | Path to compiled assets, like React build |
--siteName |
-sn |
No | Display name for your Power Pages site |
Example
pac pages upload-code-site `
--rootPath "../your-project" `
--compiledPath "./build" `
--siteName "Contoso Code Site"
If you don't have an existing project, try the sample implementations of SPA sites using React, Angular, and Vue.
Defining upload parameters with powerpages.config.json
Customize the behavior of the upload-code-site command by including a powerpages.config.json file in your site's root folder. When this file is present, run upload-code-site with only the --rootPath parameter. The command reads the remaining values from the configuration file. If you supply both command-line arguments and configuration values, the command-line arguments take precedence.
Configuration fields
| Field | Type | Required | Description |
|---|---|---|---|
siteName |
string | Yes | Display name for the Power Pages site. |
compiledPath |
string | Yes | Path to the compiled output directory (for example, the Vite dist or React build folder), relative to powerpages.config.json. |
defaultLandingPage |
string | Yes | The HTML page served when the site root is opened, relative to compiledPath (typically index.html). |
bundleFilePatterns |
string[] | No | A list of wildcard patterns identifying files in the site's web-files that the CLI removes before uploading the new build. Use this field to clean up stale, content-hashed bundles so old assets don't accumulate on the site. See Code splitting and bundle cleanup. |
includeSource |
boolean | No | When true, the command uploads your source code in addition to the compiled assets. Defaults to false. |
sourceExcludePatterns |
string[] | No | Wildcard patterns for source files to exclude from upload. Applies only when includeSource is true (for example, to skip node_modules or local environment files). |
For the most accurate and up-to-date field reference, see the powerpages.config.json schema. Add the matching $schema property to your configuration file to enable validation and autocomplete in editors that support JSON Schema.
Sample powerpages.config.json
{
"$schema": "https://www.schemastore.org/powerpages.config.json",
"siteName": "Contoso Bank",
"compiledPath": "dist",
"defaultLandingPage": "index.html",
"bundleFilePatterns": [
"index-*.js",
"index-*.css"
]
}
Download an SPA site
Use the pac pages download-code-site command to download an existing site's code to a local directory for editing or backup purposes.
Syntax
pac pages download-code-site `
[--environment <env-url-or-guid>] `
--path <local-target-folder> `
--webSiteId <site-guid> `
[--overwrite]
Parameters
| Parameter | Alias | Required | Description |
|---|---|---|---|
--environment |
-env |
No | Dataverse environment (GUID or full URL). Defaults to your active auth profile |
--path |
-p |
Yes | Local directory to download the site code |
--webSiteId |
-id |
Yes | Website record GUID of the Power Pages SPA site |
--overwrite |
-o |
No | Overwrite existing files in the target directory if they exist |
Example
pac pages download-code-site `
--environment "https://contoso.crm.dynamics.com" `
--path "./downloaded-site" `
--webSiteId "11112222-bbbb-3333-cccc-4444dddd5555" `
--overwrite
Activate and test your site
- Go to Power Pages.
- Select Inactive sites, find your site, and select Reactivate.
- When the site is active, go to your site's URL to check deployment.
Tip
Any later upload-code-site command automatically updates the active site.
Project structure and configuration
A consistent project layout helps ensure correct upload behavior.
/your-project
│
├─ src/ ← Your source code, like React components
├─ build/ ← Compiled assets, output of the `npm run build` command
├─ powerpages.config.json ← Optional CLI configuration file
└─ README.md
Use the optional powerpages.config.json file to customize how the upload-code-site command works.
Code splitting and bundle cleanup
As a single-page application grows, a single JavaScript bundle becomes large and slow to load. Modern build tools solve this with code splitting, which breaks the app into smaller chunks that the browser downloads on demand (for example, only when the user navigates to a specific route). Each chunk is emitted with a content hash in its file name, such as Dashboard-BSbmIXoe.js, so browsers can cache it for long periods and re-download it only when its contents change.
Code splitting introduces a deployment consideration unique to Power Pages SPA sites: because every build produces new hashed file names, repeated upload-code-site runs would leave the old hashed files behind on the site. Over many deployments, these orphaned chunks accumulate in the site's web-files. The bundleFilePatterns field in powerpages.config.json exists to clean them up.
Enable code splitting
Code splitting is handled by your front-end build tool, not by Power Pages, so the approach depends on the framework and bundler you use. The most common technique is to load parts of the app with dynamic imports, often applied at the route or view level so that each section downloads only when a user navigates to it (lazy loading).
Bundlers such as Vite, webpack, and esbuild can also group modules into named chunks explicitly. For the exact configuration, see the documentation for your framework and bundler.
dist/assets/
├─ index-BJltBIP-.js ← app entry
├─ index-DMwMk7hv.css ← styles
├─ Dashboard-BSbmIXoe.js ← lazy route chunk
├─ InvoiceList-DwjrGrAI.js ← lazy route chunk
└─ InvoiceDetail-D3DVGkeM.js← lazy route chunk
Whichever approach you choose, the outcome is the same, and it's the part that matters for deployment: the build emits multiple output files, each with a content hash in its name. Because those hashes change whenever a file's contents change, every build produces a different set of file names. The next sections explain how to keep your Dataverse environments clean as those names change.
How upload-code-site cleans up stale bundles
Before uploading your compiled assets, upload-code-site deletes every file in the site's web-files that matches a wildcard pattern in bundleFilePatterns, then uploads the current build. This delete-then-upload behavior keeps the deployed file set identical to your latest compiled output instead of layering each build on top of the previous one.
For the cleanup to work, the wildcard patterns in bundleFilePatterns must match the file names your build emits. There are two ways to keep them accurate, depending on how your build tool names files.
Option 1: List wildcard patterns directly
Many build tools keep a stable name prefix and change only the content hash, such as index-[hash].js. When your output file names follow a predictable pattern like this, list a wildcard pattern for each one in bundleFilePatterns. No extra tooling is needed:
{
"$schema": "https://www.schemastore.org/powerpages.config.json",
"siteName": "Contoso Bank",
"compiledPath": "dist",
"defaultLandingPage": "index.html",
"bundleFilePatterns": [
"index-*.js",
"index-*.css"
]
}
A wildcard pattern such as index-*.js matches that file on every build, regardless of the hash. Add one entry per output file, and add a new pattern whenever your build starts producing a new output file.
Option 2: Generate wildcard patterns with a post-build script
Use this approach when your output file names don't follow a predictable pattern, or when your app produces many files whose names change as you add and remove routes, which makes a hand-maintained list error-prone. A short script that runs after the build scans the compiled output and rewrites bundleFilePatterns with a wildcard pattern for each emitted file. For example, when a build tool names files as [name]-[hash].[ext], the script reduces Dashboard-BSbmIXoe.js to the pattern Dashboard-*.js.
scripts/postbuild.js:
#!/usr/bin/env node
/**
* Post-build script: scans dist/assets/ and updates powerpages.config.json
* with bundleFilePatterns that match all Vite-generated chunks.
*
* This ensures `pac pages upload-code-site` cleans up old hashed bundles
* on each deploy instead of accumulating stale files.
*
* Usage: node scripts/postbuild.js
* Or via npm: "postbuild": "node scripts/postbuild.js" in package.json
*/
import { readdirSync, readFileSync, writeFileSync } from 'fs'
import { join } from 'path'
const ROOT = join(import.meta.dirname, '..')
const DIST_ASSETS = join(ROOT, 'dist', 'assets')
const CONFIG_PATH = join(ROOT, 'powerpages.config.json')
// Vite output format: [name]-[hash].[ext]
// We want to extract "name" and "ext" to produce "name-*.ext" patterns
const HASH_PATTERN = /^(.+)-[A-Za-z0-9_-]{6,12}\.(js|css)$/
try {
const files = readdirSync(DIST_ASSETS)
const patternSet = new Set()
for (const file of files) {
const match = file.match(HASH_PATTERN)
if (match) {
const [, baseName, ext] = match
patternSet.add(`${baseName}-*.${ext}`)
}
}
const patterns = [...patternSet].sort()
if (patterns.length === 0) {
console.log('No hashed bundles found in dist/assets/ — skipping config update.')
process.exit(0)
}
// Read current config
const config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'))
const oldPatterns = config.bundleFilePatterns || []
// Check if update is needed
const oldSet = new Set(oldPatterns)
const newSet = new Set(patterns)
const changed = oldSet.size !== newSet.size || [...newSet].some(p => !oldSet.has(p))
if (!changed) {
console.log(`bundleFilePatterns already up-to-date (${patterns.length} patterns).`)
process.exit(0)
}
// Update config
config.bundleFilePatterns = patterns
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', 'utf-8')
console.log(`Updated powerpages.config.json with ${patterns.length} bundle patterns:`)
for (const p of patterns) {
console.log(` ${p}`)
}
} catch (err) {
console.error('postbuild error:', err.message)
process.exit(1)
}
Wire the script into your build so it always runs after the bundler:
{
"scripts": {
"build": "tsc -b && vite build && node scripts/postbuild.js"
}
}
Now a deployment is two commands, and the site never accumulates orphaned chunks:
npm run build
pac pages upload-code-site --rootPath .
After the build, powerpages.config.json reflects the exact current bundles, for example:
{
"$schema": "https://www.schemastore.org/powerpages.config.json",
"siteName": "Contoso Bank",
"compiledPath": "dist",
"defaultLandingPage": "index.html",
"bundleFilePatterns": [
"Dashboard-*.js",
"InvoiceDetail-*.js",
"InvoiceList-*.js",
"index-*.css",
"index-*.js"
]
}
Authentication and authorization
Power Pages SPA sites use the same security model as traditional Power Pages sites.
Configure identity providers
- Go to Power Pages.
- Find your site and select Edit.
- Select Security > Identity providers.
- Add or set up identity providers, like Microsoft Entra ID.
- Each new site automatically has a default Microsoft Entra ID provider.
Access user context in code
Get authentication metadata on the client:
Authority URL:
The authority or sign-in URL for Microsoft Entra ID is:
https://login.windows.net/<tenantId>Find the Authority URL for other configured identity providers by going to Power Pages >
<your site>> Security > Identity providers > configuration settings.User details:
window["Microsoft"].Dynamic365.Portal.User
Sample React flow
import { IconButton, Tooltip } from '@mui/material';
import {
Login,
Logout
} from '@mui/icons-material';
import React from 'react';
export const AuthButton = () => {
const username = (window as any)["Microsoft"]?.Dynamic365?.Portal?.User?.userName ?? "";
const firstName = (window as any)["Microsoft"]?.Dynamic365?.Portal?.User?.firstName ?? "";
const lastName = (window as any)["Microsoft"]?.Dynamic365?.Portal?.User?.lastName ?? "";
const tenantId = (window as any)["Microsoft"]?.Dynamic365?.Portal?.tenant ?? "";
const isAuthenticated = username !== "";
const [token, setToken] = React.useState<string>("");
React.useEffect(() => {
const fetchAntiForgeryToken = async (): Promise<string> => {
try {
const tokenEndpoint = "/_layout/tokenhtml";
const response = await fetch(tokenEndpoint, {});
if (response.status !== 200) {
throw new Error(`Failed to fetch token: ${response.status}`);
}
const tokenResponse = await response.text();
const valueString = 'value="';
const terminalString = '" />';
const valueIndex = tokenResponse.indexOf(valueString);
if (valueIndex === -1) {
throw new Error('Token not found in response');
}
const requestVerificationToken = tokenResponse.substring(
valueIndex + valueString.length,
tokenResponse.indexOf(terminalString, valueIndex)
);
return requestVerificationToken || '';
} catch (error) {
console.warn('[Impersonation] Failed to fetch anti-forgery token:', error);
return '';
}
};
const getToken = async () => {
try {
const token = await fetchAntiForgeryToken();
setToken(token);
} catch (error) {
console.error('Error fetching token:', error);
}
};
getToken();
}, []);
return (
<div className="flex items-center gap-4">
{isAuthenticated ? (
<>
<span className="text-sm">Welcome {firstName + " " + lastName}</span>
<Tooltip title="Logout">
<IconButton color="primary" onClick={() => window.location.href = "/Account/Login/LogOff?returnUrl=%2F"}>
<Logout />
</IconButton>
</Tooltip>
</>
) : (
<form action="/Account/Login/ExternalLogin" method="post">
<input name="__RequestVerificationToken" type="hidden" value={token} />
<Tooltip title="Login">
<IconButton name="provider" type="submit" color="primary" value={`https://login.windows.net/${tenantId}/`}>
<Login />
</IconButton>
</Tooltip>
</form>
)}
</div>
);
};
Use Power Pages Web APIs
Developers can use Power Pages Web APIs to load content into the UI or to create, update, and delete records. Before using these APIs, make sure that the required Web APIs are enabled and that appropriate table permissions and web roles are properly configured.
// Create query to get all cards from Dataverse
const fetchCards = async () => {
const response = await fetch("/_api/cr7ae_creditcardses");
const data = await response.json();
const cards = data.value;
const returnData = [];
// Loop through the cards and get the name and id of each card
for (let i = 0; i < cards.length; i++) {
const card = cards[i];
const cardName = card.cr7ae_name;
const cardId = card.cr7ae_creditcardsid;
const features = card.cr7ae_features
?.split(',')
.map((feature: string) => feature.trim());
const type = card.cr7ae_type;
const image = card.cr7ae_image;
const category = card.cr7ae_category
?.split(',')
.map((cat: string) => cat.trim());
// ...additional processing/pushing to returnData...
}
return returnData;
};
Set up local development by enabling Web API calls from localhost using Microsoft Entra ID authentication
Developers need faster iteration cycles, local debugging, and hot reload capabilities when building applications. SPA supports these workflows by enabling secure Web API calls from localhost using Microsoft Entra ID (Azure AD) v1 authentication.
This setup lets you:
- Run your app locally with full authentication support.
- Use modern development tools like Vite for hot reload and rapid feedback.
- Avoid CORS issues when calling Power Pages Web APIs.
- Accelerate development without deploying changes to the portal.
This configuration enables a productive local development experience for SPA, so developers can build, test, and iterate quickly with full API access and authentication support.
Important
- Use only Microsoft Entra v1 endpoints for authentication.
- Bearer authentication is supported only in portal versions 9.7.6.6 or later.
- Apply these settings only in development environments.
Configuration steps
Enable SPA authentication
- In Azure portal, open the Microsoft Entra app registered for your portal.
- Enable Single Page Application (SPA) authentication.
- Add
localhostas a redirect URI by using the Single-page application platform configuration. For more information, see How to add a redirect URI in your application.- Redirect URI:
http://localhost:<port>/.
- Redirect URI:
Add site settings
- Add these site settings in Power Pages:
Authentication/BearerAuthentication/Enabled = true Authentication/BearerAuthentication/Protocol = OpenIdConnect Authentication/BearerAuthentication/Provider = AzureADUse ADAL.js for authentication
- Implement client-side authentication by using ADAL.js.
Note
MSAL.js isn't compatible because Power Pages uses Microsoft Entra v1 endpoints, while MSAL uses v2. The issuer format differs between versions.
Add authorization header
- Include this header in all Web API requests:
Authorization: Bearer <id_token>Set site visibility to Public
- This setting lets
localhostaccess the site for development and testing purposes.
- This setting lets
Configure development proxy
- If you use Vite, add this code to
vite.config.jsto avoid CORS issues:
export default defineConfig({ plugins: [react()], server: { proxy: { '/_api': { target: 'https://site-foo.powerappsportals.com', changeOrigin: true, secure: true } } } });- If you use Vite, add this code to
Differences from existing Power Pages sites
The following table summarizes key differences between SPA sites created with this feature and traditional Power Pages sites:
| Feature | SPA site behavior |
|---|---|
| Server-side refresh | Always returns the site's root page, and the client-side router renders sub-routes. |
| Route conflicts | Client-side routes take precedence, and a hard refresh falls back to the root. |
| Page workspace | The pages workspace isn't supported. Use client routing and client site pages. For page-level security, check assigned web roles with the global user object, and conditionally render the UI. |
| Style workspace | Styling with the style workspace isn't supported. Use your framework's styling, such as CSS, CSS-in-JS, or utility classes. |
| Localization | Single-language support. Implement client-side resource loading. |
| Liquid templating | Liquid code and Liquid templates aren't supported. Access data by using your framework's template engine and Web APIs. |
FAQ
What support is available for unit and integration testing?
Currently, there's no built-in support for unit and integration testing. Makers should write and execute these tests locally or within their CI/CD pipelines.
Is there support for Power Fx integration using WebAssembly?
This capability isn't currently supported.
Is source code available in Power Pages?
Currently, makers can build websites using TypeScript or GitHub Copilot Agent. The compiled JavaScript and CSS files are accessible and can be edited in Visual Studio Code. However, direct and extensive editing of HTML files isn't currently supported.
Can I create a component externally by using this feature and bring it to a Power Pages site?
No, you can't bring an externally generated component to an existing Power Pages site by using this feature.
Can I add out-of-the-box components like lists and forms?
Adding out-of-the-box components like lists and forms isn't currently supported. However, you can build custom forms and lists by using the React framework and Web APIs.
How does source control work?
Developers can use Power Platform Git integration for source control. However, only the compiled web files are added to the repository, not the full source code.
Do these sites support SEO?
Because SPA sites are built with the React framework and use client-side rendering, SEO support is limited.
What Power Pages security and governance support do SPA sites offer?
Power Pages enforces table permissions and security web roles on Web API calls, ensuring that data access aligns with user roles. Use the window["Microsoft"].Dynamic365.Portal.User object to retrieve basic user properties and tailor experiences based on user personas.
Additionally, SPA sites support:
- Public and private site configurations
- Governance settings, including control over anonymous data access
- Authentication provider configurations
These features help ensure secure and compliant integration of custom components within Power Pages.
Related information
Feedback
Was this page helpful?
