PowerApps Portals and Power Pages: Building External-Facing Applications
{% assign anonymousId = request.cookies['anonymousId'] %} {% if anonymousId == blank %} {% assign anonymousId = now | date: '%s' | append: request.ip %} {% cookie 'anonymousId', anonymousId, 365 %} {% endif %}
Session ID: {{ anonymousId }}
Diagram: See the official Microsoft documentation for architecture details.
Configure Power Pages provider:
Portal Management app → Site Settings
Authentication/OpenAuth/AzureADB2C/Authority = https://contosoexternal.b2clogin.com/contosoexternal.onmicrosoft.com/B2C_1_signupsignin/v2.0
Authentication/OpenAuth/AzureADB2C/ClientId = {app-id}
Authentication/OpenAuth/AzureADB2C/ClientSecret = {client-secret}
Authentication/OpenAuth/AzureADB2C/RedirectUri = https://contoso.powerappsportals.com/signin-aad-b2c
Authentication/OpenAuth/AzureADB2C/ExternalLogoutEnabled = true
Authentication/Registration/Enabled = true
User registration flow:
{% comment %} Custom registration page {% endcomment %}
<form method="post" action="/account/register">
{% csrf %}
<input type="email" name="email" ="Email" required />
<input type="text" name="firstname" ="First Name" required />
<input type="text" name="lastname" ="Last Name" required />
<input type="password" name="password" ="Password" required />
<button type="submit">Register</button>
</form>
{% if errors %}
<ul class="alert alert-danger">
{% for error in errors %}
```text
<li>{{ error.message }}</li>```
{% endfor %}
</ul>
{% endif %}
Local Authentication
Username/password accounts:
Architecture Overview: Site Settings → Authentication Registration Enabled = true
Password reset:
{% comment %} Forgot password page {% endcomment %}
<form method="post" action="/account/forgotpassword">
{% csrf %}
<input type="email" name="email" ="Email" required />
<button type="submit">Send Reset Link</button>
</form>
{% comment %} Email template for reset link {% endcomment %}
Subject: Password Reset Request
Click the link below to reset your password:
{{ reset_url }}
This link expires in 24 hours.
Social Identity Providers
Microsoft, Google, Facebook, LinkedIn:
Architecture Overview: Portal Management app → Site Settings
Sign-in page:
<h2>Sign in</h2>
<a href="/signin-microsoft" class="btn btn-primary">
<i class="fab fa-microsoft"></i> Sign in with Microsoft
</a>
<a href="/signin-google" class="btn btn-danger">
<i class="fab fa-google"></i> Sign in with Google
</a>
<a href="/signin-facebook" class="btn btn-info">
<i class="fab fa-facebook"></i> Sign in with Facebook
</a>
<hr />
<form method="post" action="/account/login">
{% csrf %}
<input type="text" name="username" ="Username" />
<input type="password" name="password" ="Password" />
<button type="submit">Sign in</button>
</form>
<a href="/account/register">Create account</a>
<a href="/account/forgotpassword">Forgot password?</a>
Web Roles and Permissions
Web Roles
Role hierarchy:
Architecture Overview: Web Roles (Portal Management app):
Create web role:
Architecture Overview: Portal Management app → Web Roles → New
Table Permissions
Row-level security:
Architecture Overview: Portal Management app → Table Permissions → New
Access type examples:
Architecture Overview: Contact (user's own records):
Multiple permissions:
Architecture Overview: Customer can:
Web Templates (Liquid)
Liquid Template Language
Basic syntax:
{% comment %} Variables {% endcomment %}
{% assign userName = user.fullname %}
<p>Welcome, {{ userName }}!</p>
{% comment %} Conditionals {% endcomment %}
{% if user %}
<p>Signed in as {{ user.email }}</p>
{% else %}
<a href="/signin">Sign in</a>
{% endif %}
{% comment %} Loops {% endcomment %}
<ul>
{% for case in cases %}
<li>{{ case.title }} - {{ case.statuscode.label }}</li>
{% endfor %}
</ul>
{% comment %} Filters {% endcomment %}
<p>Posted {{ case.createdon | date: '%B %d, %Y' }}</p>
<p>{{ case.description | truncate: 100 }}</p>
Dataverse Queries
EntityList (multiple records):
{% entitylist
name: "Active Cases"
pagesize: 10
filter: "statuscode eq 1"
order: "createdon desc"
%}
<table class="table">
```text
<thead>
<tr>
<th>Case Number</th>
<th>Title</th>
<th>Status</th>
<th>Created</th>
</tr>
</thead>
<tbody>
{% for case in entitylist.records %}
<tr>
<td>{{ case.ticketnumber }}</td>
<td><a href="/cases/{{ case.id }}">{{ case.title }}</a></td>
<td>{{ case.statuscode.label }}</td>
<td>{{ case.createdon | date: '%m/%d/%Y' }}</td>
</tr>
{% endfor %}
</tbody>```
</table>
{% comment %} Pagination {% endcomment %}
<nav>
```text
<ul class="pagination">
{% if entitylist.page > 1 %}
<li><a href="?page={{ entitylist.page | minus: 1 }}">Previous</a></li>
{% endif %}
<li>Page {{ entitylist.page }} of {{ entitylist.total_pages }}</li>
{% if entitylist.page < entitylist.total_pages %}
<li><a href="?page={{ entitylist.page | plus: 1 }}">Next</a></li>
{% endif %}
</ul>```
</nav>
{% endentitylist %}
EntityView (single record):
{% entityview
logical_name: 'incident'
id: request.params.id
%}
<h1>{{ entityview.title }}</h1>
<dl>
```text
<dt>Case Number</dt>
<dd>{{ entityview.ticketnumber }}</dd>
<dt>Status</dt>
<dd>{{ entityview.statuscode.label }}</dd>
<dt>Priority</dt>
<dd>{{ entityview.prioritycode.label }}</dd>
<dt>Created</dt>
<dd>{{ entityview.createdon | date: '%B %d, %Y %I:%M %p' }}</dd>
<dt>Description</dt>
<dd>{{ entityview.description }}</dd>```
</dl>
{% if entityview.customer %}
```text
<p>Customer: {{ entityview.customer.fullname }}</p>```
{% endif %}
{% endentityview %}
Reusable Components
Header template:
{% comment %} Web Template: Header {% endcomment %}
<header class="site-header">
<div class="container">
```text
<a href="/" class="logo">
<img src="{{ site.logo }}" alt="{{ site.title }}" />
</a>
<nav>
<ul class="nav">
<li><a href="/">Home</a></li>
<li><a href="/products">Products</a></li>
<li><a href="/support">Support</a></li>
{% if user %}
<li><a href="/account">My Account</a></li>
<li><a href="/signout">Sign Out</a></li>
{% else %}
<li><a href="/signin">Sign In</a></li>
{% endif %}
</ul>
</nav>```
</div>
</header>
{% comment %} Include in page template {% endcomment %}
{% include 'Header' %}
Entity Forms
Create Form
Configuration:
Diagram: See the official Microsoft documentation for architecture details.
Embed in web page:
{% comment %} Web Template: Create Case {% endcomment %}
<h1>Create Support Case</h1>
<p>Describe your issue and we'll get back to you within 24 hours.</p>
{% entityform name: 'Create Case Form' %}
{% comment %} Custom styling {% endcomment %}
<style>
.entityform .form-group { margin-bottom: 20px; }
.entityform label { font-weight: bold; }
.entityform .required:after { content: "*"; color: red; }
</style>
Edit Form
Update existing record:
Diagram: See the official Microsoft documentation for architecture details.
Edit page with security:
{% entityview logical_name: 'incident', id: request.params.id %}
{% comment %} Check user owns this case {% endcomment %}
{% if entityview.customer.id == user.id %}
```text
<h1>Edit Case: {{ entityview.ticketnumber }}</h1>
{% entityform name: 'Edit Case Form' %}
{% else %}
<div class="alert alert-danger">
You do not have permission to edit this case.
</div>```
{% endif %}
{% endentityview %}
Read-Only Form
View mode:
Entity Form: View Case Form
Mode: ReadOnly
Form Name: Information
Show Unsupported Fields: No (hide complex controls)
Embed:
{% entityform name: 'View Case Form' %}
Entity Lists
Basic List
Configuration:
Architecture Overview: Portal Management app → Entity Lists → New
Embed in page:
{% comment %} Web Template: My Cases {% endcomment %}
<h1>My Support Cases</h1>
{% entitylist
name: "My Cases List"
%}
{% comment %} Custom rendering instead of default grid {% endcomment %}
<div class="cases">
{% for case in entitylist.records %}
```text
<div class="case-card">
<h3>
<a href="/cases/{{ case.id }}">{{ case.title }}</a>
</h3>
<p class="case-number">Case #{{ case.ticketnumber }}</p>
<p class="case-status">
<span class="badge badge-{{ case.statuscode.value }}">
{{ case.statuscode.label }}
</span>
</p>
<p class="case-date">Created {{ case.createdon | date: '%m/%d/%Y' }}</p>
<p>{{ case.description | truncate: 150 }}</p>
</div>```
{% endfor %}
</div>
{% include 'Pagination' with entitylist %}
{% endentitylist %}
Advanced Filtering
Filter configuration:
Diagram: See the official Microsoft documentation for architecture details.
Custom filter UI:
<form method="get" class="case-filters">
<label>Status:</label>
<select name="statuscode">
```text
<option value="">All</option>
<option value="1" {% if request.params.statuscode == "1" %}selected{% endif %}>Active</option>
<option value="5" {% if request.params.statuscode == "5" %}selected{% endif %}>Resolved</option>
<option value="6" {% if request.params.statuscode == "6" %}selected{% endif %}>Closed</option>```
</select>
<label>Priority:</label>
<select name="prioritycode">
```text
<option value="">All</option>
<option value="1" {% if request.params.prioritycode == "1" %}selected{% endif %}>High</option>
<option value="2" {% if request.params.prioritycode == "2" %}selected{% endif %}>Normal</option>
<option value="3" {% if request.params.prioritycode == "3" %}selected{% endif %}>Low</option>```
</select>
<button type="submit">Filter</button>
</form>
{% entitylist
name: "My Cases List"
filter: "statuscode eq {{ request.params.statuscode | default: '' }} and prioritycode eq {{ request.params.prioritycode | default: '' }}"
%}
{% comment %} Render list {% endcomment %}
{% endentitylist %}
Site Settings and Configuration
Key site settings:
Diagram: See the official Microsoft documentation for architecture details.
Best Practices
- Authentication: Use Azure AD B2C for scalability, not local accounts
- Permissions: Always configure table permissions for authenticated users
- Performance: Enable output caching, optimize Dataverse queries
- Security: Enforce HTTPS, implement rate limiting, use CAPTCHA
- SEO: Configure meta tags, sitemap, friendly URLs
- Monitoring: Enable Application Insights for portal telemetry
Troubleshooting
User cannot access page:
- Check web page Publishing State = Published
- Verify Web Page Access Control Rules (if restricted)
- Check user has assigned Web Role
- Confirm table permissions granted to web role
Entity form not saving:
- Verify table permissions include Create/Write privileges
- Check Dataverse form exists and is published
- Review required fields are populated
- Check for business rules/plugins blocking save
Performance issues:
- Enable output caching for static pages
- Reduce entity list page size (10-20 records)
- Optimize Dataverse views (limit columns, add indexes)
- Use CDN for images/CSS/JavaScript
Architecture Decision and Tradeoffs
When designing low-code development solutions with Power Apps, consider these key architectural trade-offs:
| Approach | Best For | Tradeoff |
|---|---|---|
| Managed / platform service | Rapid delivery, reduced ops burden | Less customisation, potential vendor lock-in |
| Custom / self-hosted | Full control, advanced tuning | Higher operational overhead and cost |
Recommendation: Start with the managed approach for most workloads and move to custom only when specific requirements demand it.
Validation and Versioning
- Last validated: April 2026
- Validate examples against your tenant, region, and SKU constraints before production rollout.
- Keep module, CLI, and SDK versions pinned in automation pipelines and review quarterly.
Security and Governance Considerations
- Apply least-privilege access using RBAC roles and just-in-time elevation for admin tasks.
- Store secrets in managed secret stores and avoid embedding credentials in scripts or source files.
- Enable audit logging, data protection policies, and periodic access reviews for regulated workloads.
Cost and Performance Notes
- Define budgets and alerts, then monitor usage and cost trends continuously after go-live.
- Baseline performance with synthetic and real-user checks before and after major changes.
- Scale resources with measured thresholds and revisit sizing after usage pattern changes.
Official Microsoft References
- https://learn.microsoft.com/power-apps/
- https://learn.microsoft.com/power-platform/admin/
- https://learn.microsoft.com/power-platform/guidance/
Public Examples from Official Sources
- These examples are sourced from official public Microsoft documentation and sample repositories.
- Documentation examples: https://learn.microsoft.com/power-apps/
- Sample repositories: https://github.com/microsoft/PowerApps-Samples
- Prefer adapting these examples to your tenant, subscriptions, and governance requirements before production use.
Key Takeaways
- Power Pages extends Dataverse to external users (customers, partners)
- Azure AD B2C provides scalable authentication for thousands of users
- Web roles and table permissions enforce row-level security
- Liquid templates enable custom page layouts and Dataverse queries
- Entity forms and lists provide no-code CRUD operations
- Portal Management app configures all site settings and permissions
Next Steps
- Configure Azure Front Door for global CDN and WAF protection
- Implement multi-factor authentication for sensitive operations
- Add custom APIs with Azure Functions for complex business logic
- Enable Application Insights for monitoring and diagnostics
Additional Resources
Extend Dataverse beyond the organization.
Discussion