Home / PowerApps / PowerApps Portals and Power Pages: Building External-Facing Applications
PowerApps

PowerApps Portals and Power Pages: Building External-Facing Applications

Build external-facing web applications with Power Pages (formerly PowerApps Portals) featuring anonymous access, authenticated users with Azure AD B2C, Liqui...

What you will learn

Practical execution with concise explanations, real implementation patterns, and production-ready recommendations.

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

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)

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

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

  1. Authentication: Use Azure AD B2C for scalability, not local accounts
  2. Permissions: Always configure table permissions for authenticated users
  3. Performance: Enable output caching, optimize Dataverse queries
  4. Security: Enforce HTTPS, implement rate limiting, use CAPTCHA
  5. SEO: Configure meta tags, sitemap, friendly URLs
  6. 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