Embedded Analytics: Power BI Embedded Implementation
Introduction
Power BI Embedded transforms static business applications into intelligent, data-driven experiences by bringing interactive analytics directly into custom web applications, mobile apps, and portals. Whether you're building a SaaS product, customer-facing portal, or internal line-of-business application, Power BI Embedded enables you to deliver enterprise-grade analytics without users ever leaving your application.
The difference between Power BI Embedded and traditional Power BI Service is fundamental: Embedded is designed for programmatic control, white-label experiences, and application integration, while the Service is optimized for self-service analytics and collaboration. With Embedded, you control the authentication flow, branding, navigation, and user experience while leveraging Power BI's powerful visualization and analytical engine.
However, implementing Power BI Embedded successfully requires careful planning across multiple dimensions: capacity sizing and cost optimization, secure authentication patterns, multi-tenant architecture decisions, JavaScript API mastery, lifecycle management, and operational monitoring. A poorly architected embedded solution can lead to security vulnerabilities, performance bottlenecks, cost overruns, and frustrated end users.
This comprehensive guide covers everything you need to build production-ready Power BI Embedded solutions: from capacity planning and SKU selection through service principal setup, JavaScript API integration patterns, multi-tenant design strategies, token management, real-world code examples, monitoring automation, troubleshooting, and operational best practices.
Prerequisites
- Azure subscription with Contributor access
- Power BI Pro or Premium Per User (PPU) license
- .NET 6+ or Node.js 16+ for backend development
- JavaScript/TypeScript knowledge for frontend integration
- (Optional) Multi-tenant application architecture understanding
- (Optional) Azure App Service or Azure Functions for hosting
Understanding Power BI Embedded Architecture
Embedding Architecture Overview
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β YOUR WEB APPLICATION β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Frontend (Browser) β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β React/Angular/Vue Application β β
β β - powerbi-client.js library β β
β β - Embed container (div) β β
β β - Event handlers (loaded, error, dataSelected) β β
β ββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββββ β
β β 1. Request embed token β
β βΌ β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Backend API (.NET/Node.js/Python) β β
β β - Service Principal authentication β β
β β - Generate embed token via Power BI REST API β β
β β - Apply RLS (Row-Level Security) if needed β β
β β - Return token + embedUrl + reportId β β
β ββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββββββββ
β 2. Authenticate & get token
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MICROSOFT AZURE β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Microsoft Entra ID (Azure AD) β
β - Service Principal (App Registration) β
β - Client ID + Client Secret β
β - Grant Power BI API permissions β
β β
β Power BI Embedded Capacity (Azure Resource) β
β - A SKU (Azure capacity: A1-A6) β
β - EM SKU (Embedded capacity: EM1-EM3, legacy) β
β - F SKU (Fabric capacity: F2-F2048) β
β - Auto-pause when idle (cost savings) β
β β
β Power BI Service β
β - Workspaces assigned to Embedded capacity β
β - Reports, datasets, dataflows β
β - Refresh schedules, gateway connections β
βββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββ
β 3. Load report with token
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β End User Browser β
β - Embedded Power BI report renders in iframe β
β - Interactive visuals, filters, drill-through β
β - No Power BI branding (white-label) β
β - Token expires after 60 minutes (regenerate required) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Key Concepts
1. Embed Token vs. User Token:
- Embed Token: Short-lived (default: 60 min), generated server-side, grants access to specific report/dataset
- User Token: User's Azure AD token, used for Power BI Service access (not for Embedded)
2. Capacity Assignment:
- Workspaces must be assigned to Embedded capacity (A/EM/F SKU)
- Reports in non-Embedded capacity cannot be embedded
- Multiple workspaces can share same capacity
3. Service Principal:
- Azure AD application identity (not a user)
- Granted Power BI workspace access programmatically
- Generates embed tokens without user interaction
4. White-Label Experience:
- No Power BI branding visible to end users
- Custom navigation, filters, and UI
- Application owns the user authentication
Azure Capacity Planning
SKU Selection Guide
Power BI Embedded SKU Comparison:
ββββββββββββ¬βββββββββββ¬βββββββββββ¬βββββββββββββββ¬ββββββββββββββββββ
β SKU β vCores β Memory β Cost/Hour β Use Case β
ββββββββββββΌβββββββββββΌβββββββββββΌβββββββββββββββΌββββββββββββββββββ€
β A1 β 1 β 3 GB β ~$1.00 β Dev/Test only β
β A2 β 2 β 5 GB β ~$2.00 β Small prod β
β A3 β 4 β 10 GB β ~$4.00 β Medium prod β
β A4 β 8 β 25 GB β ~$8.00 β Production β
β A5 β 16 β 50 GB β ~$16.00 β Large prod β
β A6 β 32 β 100 GB β ~$32.00 β Enterprise β
ββββββββββββ΄βββββββββββ΄βββββββββββ΄βββββββββββββββ΄ββββββββββββββββββ
β F2-F64 β 2-64 β Varies β ~$0.50-96 β Fabric (newer) β
ββββββββββββ΄βββββββββββ΄βββββββββββ΄βββββββββββββββ΄ββββββββββββββββββ
Cost Optimization Strategies:
1. Auto-Pause: Capacity pauses after 10 minutes of inactivity
- Saves ~70% on costs for intermittent usage
- 2-minute startup delay when resuming
2. Auto-Scale: Automatically scale up during high load
- Prevents capacity overload
- Costs increase during scale period
3. Right-Sizing:
- Monitor vCore utilization (target: 50-70% average)
- Scale up if sustained >80% for >30 minutes
- Scale down if sustained <30% for >7 days
Capacity Sizing Calculator
# Estimate required capacity based on usage
function Get-EmbeddedCapacityRecommendation {
param(
[int]$ConcurrentUsers = 100,
[int]$ReportsPerUser = 2,
[int]$VisualsPerReport = 10,
[decimal]$AvgQueryTimeMs = 500
)
# Rough estimation formula
$totalVisuals = $ConcurrentUsers * $ReportsPerUser * $VisualsPerReport
$queriesPerSecond = $totalVisuals * (1000 / $AvgQueryTimeMs)
# Capacity benchmarks (queries/sec sustained)
$capacityBenchmarks = @{
"A1" = 5
"A2" = 10
"A3" = 20
"A4" = 40
"A5" = 80
"A6" = 160
}
Write-Host "`n=== Embedded Capacity Recommendation ===" -ForegroundColor Cyan
Write-Host "Concurrent Users: $ConcurrentUsers"
Write-Host "Reports per User: $ReportsPerUser"
Write-Host "Visuals per Report: $VisualsPerReport"
Write-Host "Avg Query Time: $($AvgQueryTimeMs)ms"
Write-Host "Estimated Queries/Sec: $([math]::Round($queriesPerSecond, 2))"
Write-Host "`nRecommended SKU:" -ForegroundColor Yellow
foreach ($sku in $capacityBenchmarks.GetEnumerator() | Sort-Object Value) {
if ($queriesPerSecond -le $sku.Value) {
Write-Host " $($sku.Key) - Supports up to $($sku.Value) queries/sec" -ForegroundColor Green
Write-Host " Estimated Cost: ~`$$($sku.Key.Substring(1))/hour (with auto-pause: ~`$$([math]::Round([int]$sku.Key.Substring(1) * 0.3, 2))/hour average)"
return
}
}
Write-Host " β οΈ Workload exceeds A6 capacity - consider multiple capacities or optimization" -ForegroundColor Red
}
# Example usage
Get-EmbeddedCapacityRecommendation -ConcurrentUsers 50 -ReportsPerUser 1 -VisualsPerReport 8
Create Embedded Capacity
# Azure CLI: Create Power BI Embedded capacity
$resourceGroup = "rg-powerbi-embedded"
$location = "East US"
$capacityName = "pbiembed-prod-001"
$sku = "A3" # 4 vCores, 10GB RAM
# Create resource group
az group create --name $resourceGroup --location $location
# Create Power BI Embedded capacity
az powerbi embedded-capacity create `
--resource-group $resourceGroup `
--name $capacityName `
--location $location `
--sku-name $sku `
--sku-tier "PBIE_Azure" `
--administration-members "user@contoso.com"
# Enable auto-pause (saves cost when idle)
az powerbi embedded-capacity update `
--resource-group $resourceGroup `
--name $capacityName `
--suspend-on-idle Enabled
Write-Host "β
Capacity created: $capacityName ($sku)" -ForegroundColor Green
Write-Host "Assign workspaces to this capacity in Power BI Service settings"
Service Principal Setup
Create Azure AD App Registration
# Create service principal for Power BI Embedded
$appName = "PowerBI-Embedded-App"
$tenantId = "your-tenant-id"
# Create app registration
$app = az ad app create --display-name $appName --query "{appId:appId,id:id}" -o json | ConvertFrom-Json
$appId = $app.appId
$objectId = $app.id
Write-Host "App ID (Client ID): $appId" -ForegroundColor Cyan
Write-Host "Object ID: $objectId"
# Create client secret
$secret = az ad app credential reset --id $appId --query "password" -o tsv
Write-Host "Client Secret: $secret" -ForegroundColor Yellow
Write-Host "β οΈ Save this secret securely - it won't be shown again!" -ForegroundColor Red
# Add Power BI API permissions
az ad app permission add --id $appId --api 00000009-0000-0000-c000-000000000000 --api-permissions c2f89d3e-f2a0-4c9f-a7f5-6e3e9c8b4e5f=Scope
# Grant admin consent (requires Global Admin or Application Admin)
az ad app permission admin-consent --id $appId
Write-Host "`nβ
Service Principal created successfully" -ForegroundColor Green
Write-Host "`nNext Steps:"
Write-Host "1. Enable Power BI Service Admin settings:"
Write-Host " - Allow service principals to use Power BI APIs"
Write-Host " - Add security group containing this service principal"
Write-Host "2. Grant service principal access to Power BI workspaces"
Power BI Service Configuration
Enable Service Principal in Power BI Admin Portal:
1. Navigate to: https://app.powerbi.com/admin-portal
2. Tenant settings β Developer settings
3. Enable "Allow service principals to use Power BI APIs"
4. Apply to: Specific security groups
5. Create Azure AD security group:
- Name: "PowerBI-Embedded-ServicePrincipals"
- Add the service principal app
6. Add security group to the setting
7. Click Apply
Grant Workspace Access:
1. Open workspace in Power BI Service
2. Workspace settings β Access
3. Add service principal:
- Search by App ID
- Grant "Member" or "Admin" role
4. Save
Assign Workspace to Capacity:
1. Workspace settings β Premium
2. Select your Embedded capacity
3. Click Apply
4. Wait for workspace migration (1-2 minutes)
Backend Implementation (.NET)
Embed Token Generation Service
using Microsoft.PowerBI.Api;
using Microsoft.PowerBI.Api.Models;
using Microsoft.Rest;
using Microsoft.Identity.Client;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
public class PowerBIEmbedService
{
private readonly string _tenantId;
private readonly string _clientId;
private readonly string _clientSecret;
private readonly string _authorityUrl;
private readonly string _powerBIApiUrl = "https://api.powerbi.com";
private readonly string[] _scopes = new[] { "https://analysis.windows.net/powerbi/api/.default" };
public PowerBIEmbedService(string tenantId, string clientId, string clientSecret)
{
_tenantId = tenantId;
_clientId = clientId;
_clientSecret = clientSecret;
_authorityUrl = $"https://login.microsoftonline.com/{tenantId}";
}
// Get Azure AD token using service principal
private async Task<string> GetAccessTokenAsync()
{
var clientApp = ConfidentialClientApplicationBuilder
.Create(_clientId)
.WithClientSecret(_clientSecret)
.WithAuthority(new Uri(_authorityUrl))
.Build();
var authResult = await clientApp.AcquireTokenForClient(_scopes).ExecuteAsync();
return authResult.AccessToken;
}
// Generate embed token for a report
public async Task<EmbedConfig> GetEmbedConfigAsync(
Guid workspaceId,
Guid reportId,
string username = null,
string[] roles = null)
{
try
{
// Get Azure AD token
var accessToken = await GetAccessTokenAsync();
var tokenCredentials = new TokenCredentials(accessToken, "Bearer");
// Create Power BI client
using (var client = new PowerBIClient(new Uri(_powerBIApiUrl), tokenCredentials))
{
// Get report details
var report = await client.Reports.GetReportInGroupAsync(workspaceId, reportId);
// Generate embed token
var generateTokenRequestParameters = new GenerateTokenRequest(
accessLevel: "View",
datasetId: report.DatasetId
);
// Apply Row-Level Security (RLS) if specified
if (!string.IsNullOrEmpty(username) && roles != null && roles.Length > 0)
{
var effectiveIdentity = new EffectiveIdentity(
username: username,
roles: roles,
datasets: new List<string> { report.DatasetId }
);
generateTokenRequestParameters.Identities = new List<EffectiveIdentity> { effectiveIdentity };
}
var tokenResponse = await client.Reports.GenerateTokenInGroupAsync(
workspaceId,
reportId,
generateTokenRequestParameters
);
// Return embed configuration
return new EmbedConfig
{
EmbedToken = tokenResponse.Token,
EmbedUrl = report.EmbedUrl,
ReportId = report.Id.ToString(),
ReportName = report.Name,
TokenExpiry = tokenResponse.Expiration.Value
};
}
}
catch (HttpOperationException ex)
{
throw new Exception($"Power BI API Error: {ex.Response.Content}", ex);
}
}
// Generate embed token for multiple reports (dashboard scenario)
public async Task<EmbedConfig> GetEmbedConfigForMultipleReportsAsync(
Guid workspaceId,
List<Guid> reportIds)
{
var accessToken = await GetAccessTokenAsync();
var tokenCredentials = new TokenCredentials(accessToken, "Bearer");
using (var client = new PowerBIClient(new Uri(_powerBIApiUrl), tokenCredentials))
{
var reports = new List<Report>();
var datasetIds = new List<string>();
// Collect all reports and dataset IDs
foreach (var reportId in reportIds)
{
var report = await client.Reports.GetReportInGroupAsync(workspaceId, reportId);
reports.Add(report);
if (!datasetIds.Contains(report.DatasetId))
{
datasetIds.Add(report.DatasetId);
}
}
// Generate embed token for all reports
var generateTokenRequestParameters = new GenerateTokenRequest(
accessLevel: "View",
datasetId: datasetIds[0] // Primary dataset
);
// Add additional datasets
if (datasetIds.Count > 1)
{
generateTokenRequestParameters.Datasets = datasetIds.Select(id =>
new GenerateTokenRequestDataset(id)).ToList();
}
var tokenResponse = await client.Reports.GenerateTokenForMultipleReportsAsync(
workspaceId,
reportIds,
generateTokenRequestParameters
);
return new EmbedConfig
{
EmbedToken = tokenResponse.Token,
Reports = reports.Select(r => new EmbedReport
{
ReportId = r.Id.ToString(),
ReportName = r.Name,
EmbedUrl = r.EmbedUrl
}).ToList(),
TokenExpiry = tokenResponse.Expiration.Value
};
}
}
}
// Data models
public class EmbedConfig
{
public string EmbedToken { get; set; }
public string EmbedUrl { get; set; }
public string ReportId { get; set; }
public string ReportName { get; set; }
public DateTime TokenExpiry { get; set; }
public List<EmbedReport> Reports { get; set; }
}
public class EmbedReport
{
public string ReportId { get; set; }
public string ReportName { get; set; }
public string EmbedUrl { get; set; }
}
API Controller Example
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using System;
using System.Threading.Tasks;
[ApiController]
[Route("api/[controller]")]
[Authorize] // Your app's authentication
public class PowerBIController : ControllerBase
{
private readonly PowerBIEmbedService _embedService;
private readonly IConfiguration _configuration;
public PowerBIController(PowerBIEmbedService embedService, IConfiguration configuration)
{
_embedService = embedService;
_configuration = configuration;
}
[HttpGet("embed-token/{reportName}")]
public async Task<IActionResult> GetEmbedToken(string reportName)
{
try
{
// Get authenticated user from your app
var currentUser = User.Identity.Name;
// Map report name to workspace/report IDs (from configuration)
var workspaceId = Guid.Parse(_configuration[$"PowerBI:Reports:{reportName}:WorkspaceId"]);
var reportId = Guid.Parse(_configuration[$"PowerBI:Reports:{reportName}:ReportId"]);
// Optional: Apply RLS based on user
string[] roles = GetUserRolesForRLS(currentUser);
// Generate embed configuration
var embedConfig = await _embedService.GetEmbedConfigAsync(
workspaceId,
reportId,
currentUser,
roles
);
return Ok(embedConfig);
}
catch (Exception ex)
{
return StatusCode(500, new { error = ex.Message });
}
}
private string[] GetUserRolesForRLS(string username)
{
// Your logic to determine RLS roles
// Example: User's department, region, or customer ID
return new[] { "SalesRegion:East" };
}
}
Frontend Implementation (React)
Power BI React Component
import React, { useEffect, useRef, useState } from 'react';
import { models, service, factories } from 'powerbi-client';
interface PowerBIEmbedProps {
reportName: string;
filters?: models.IFilter[];
onLoaded?: () => void;
onError?: (error: any) => void;
}
const PowerBIEmbed: React.FC<PowerBIEmbedProps> = ({
reportName,
filters,
onLoaded,
onError
}) => {
const reportContainer = useRef<HTMLDivElement>(null);
const [report, setReport] = useState<service.Report | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
embedReport();
return () => {
// Cleanup: remove report on unmount
if (report) {
report.off('loaded');
report.off('error');
factories.powerBIReportFactory.remove(reportContainer.current!);
}
};
}, [reportName]);
const embedReport = async () => {
try {
setLoading(true);
setError(null);
// Fetch embed configuration from backend
const response = await fetch(`/api/powerbi/embed-token/${reportName}`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('authToken')}`
}
});
if (!response.ok) {
throw new Error(`Failed to get embed token: ${response.statusText}`);
}
const embedConfig = await response.json();
// Power BI embed configuration
const config: models.IReportEmbedConfiguration = {
type: 'report',
tokenType: models.TokenType.Embed,
accessToken: embedConfig.embedToken,
embedUrl: embedConfig.embedUrl,
id: embedConfig.reportId,
settings: {
panes: {
filters: {
expanded: false,
visible: true
},
pageNavigation: {
visible: true
}
},
background: models.BackgroundType.Transparent,
layoutType: models.LayoutType.Custom,
customLayout: {
displayOption: models.DisplayOption.FitToWidth
}
},
filters: filters || []
};
// Embed report
const powerbi = new service.Service(
factories.hpmFactory,
factories.wpmpFactory,
factories.routerFactory
);
const embeddedReport = powerbi.embed(
reportContainer.current!,
config
) as service.Report;
// Handle loaded event
embeddedReport.on('loaded', () => {
console.log('Report loaded successfully');
setLoading(false);
onLoaded?.();
});
// Handle error event
embeddedReport.on('error', (event) => {
const errorDetail = event.detail;
console.error('Report error:', errorDetail);
setError(errorDetail.message);
setLoading(false);
onError?.(errorDetail);
});
// Handle data selected event (optional)
embeddedReport.on('dataSelected', (event) => {
console.log('Data selected:', event.detail);
});
setReport(embeddedReport);
// Refresh token before expiry
scheduleTokenRefresh(embedConfig.tokenExpiry, embeddedReport);
} catch (err: any) {
console.error('Embedding error:', err);
setError(err.message);
setLoading(false);
onError?.(err);
}
};
const scheduleTokenRefresh = (tokenExpiry: string, embeddedReport: service.Report) => {
const expiryTime = new Date(tokenExpiry).getTime();
const currentTime = Date.now();
const timeUntilExpiry = expiryTime - currentTime;
// Refresh token 5 minutes before expiry
const refreshTime = timeUntilExpiry - (5 * 60 * 1000);
if (refreshTime > 0) {
setTimeout(async () => {
try {
// Fetch new token
const response = await fetch(`/api/powerbi/embed-token/${reportName}`);
const embedConfig = await response.json();
// Update token
await embeddedReport.setAccessToken(embedConfig.embedToken);
console.log('Token refreshed successfully');
// Schedule next refresh
scheduleTokenRefresh(embedConfig.tokenExpiry, embeddedReport);
} catch (err) {
console.error('Token refresh failed:', err);
}
}, refreshTime);
}
};
const applyFilters = async (newFilters: models.IFilter[]) => {
if (report) {
try {
await report.updateFilters(models.FiltersOperations.Replace, newFilters);
console.log('Filters applied successfully');
} catch (err) {
console.error('Failed to apply filters:', err);
}
}
};
return (
<div style={{ position: 'relative', width: '100%', height: '600px' }}>
{loading && (
<div style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)'
}}>
Loading report...
</div>
)}
{error && (
<div style={{ color: 'red', padding: '20px' }}>
Error: {error}
</div>
)}
<div
ref={reportContainer}
style={{ width: '100%', height: '100%' }}
/>
</div>
);
};
export default PowerBIEmbed;
Usage Example
// App.tsx
import React from 'react';
import PowerBIEmbed from './components/PowerBIEmbed';
import { models } from 'powerbi-client';
const App: React.FC = () => {
const handleReportLoaded = () => {
console.log('Sales report loaded successfully');
};
const handleReportError = (error: any) => {
console.error('Sales report error:', error);
// Send to monitoring service
};
// Apply filters programmatically
const filters: models.IBasicFilter[] = [
{
$schema: "http://powerbi.com/product/schema#basic",
target: {
table: "Sales",
column: "Region"
},
operator: "In",
values: ["East", "West"],
filterType: models.FilterType.Basic
}
];
return (
<div className="App">
<h1>Sales Dashboard</h1>
<PowerBIEmbed
reportName="sales-overview"
filters={filters}
onLoaded={handleReportLoaded}
onError={handleReportError}
/>
</div>
);
};
export default App;
Multi-Tenant Architecture
Strategy 1: Workspace-Per-Tenant
Architecture: Each tenant gets dedicated workspace
Pros:
β
Complete data isolation
β
Tenant-specific customization
β
Independent refresh schedules
β
Easy to add/remove tenants
Cons:
β Higher management overhead
β Workspace limits (1000 per capacity)
β More complex deployment
Use Case: SaaS with <100 customers, strong isolation requirements
public class MultiTenantWorkspaceService
{
private readonly PowerBIEmbedService _embedService;
private readonly Dictionary<string, TenantWorkspace> _tenantWorkspaces;
public async Task<EmbedConfig> GetTenantReportAsync(string tenantId, string reportType)
{
// Look up tenant's dedicated workspace
if (!_tenantWorkspaces.ContainsKey(tenantId))
{
throw new Exception($"Workspace not found for tenant: {tenantId}");
}
var workspace = _tenantWorkspaces[tenantId];
var reportId = workspace.Reports[reportType];
// Generate embed token (no RLS needed - workspace is already isolated)
return await _embedService.GetEmbedConfigAsync(workspace.WorkspaceId, reportId);
}
public async Task ProvisionTenantWorkspaceAsync(string tenantId, string tenantName)
{
// Create new workspace for tenant
// Deploy reports from template
// Assign to capacity
// Update tenant mapping
}
}
public class TenantWorkspace
{
public Guid WorkspaceId { get; set; }
public string TenantId { get; set; }
public Dictionary<string, Guid> Reports { get; set; }
}
Strategy 2: Shared Workspace with RLS
Architecture: Single workspace, Row-Level Security for data isolation
Pros:
β
Lower management overhead
β
Scales to thousands of tenants
β
Single deployment/updates
β
Efficient capacity usage
Cons:
β Complex RLS configuration
β All tenants impacted by single tenant's issues
β Shared refresh schedule
Use Case: SaaS with 100+ customers, standardized reports
public class MultiTenantRLSService
{
private readonly PowerBIEmbedService _embedService;
private readonly Guid _sharedWorkspaceId = Guid.Parse("shared-workspace-guid");
public async Task<EmbedConfig> GetTenantReportAsync(string tenantId, Guid reportId)
{
// Apply RLS with tenant identifier
var username = $"tenant:{tenantId}";
var roles = new[] { "TenantUser" };
return await _embedService.GetEmbedConfigAsync(
_sharedWorkspaceId,
reportId,
username,
roles
);
}
}
RLS DAX Example:
// In Power BI Desktop: Modeling β Manage Roles β Create "TenantUser" role
// RLS Rule on Sales table:
[TenantID] = RIGHT(USERNAME(), LEN(USERNAME()) - 7)
// USERNAME() returns "tenant:12345"
// RIGHT() extracts "12345"
// Only rows matching TenantID are visible
Strategy 3: Hybrid Approach
Architecture: Premium tenants get dedicated workspace, others share with RLS
Pros:
β
Flexibility for different SLA tiers
β
Balance scalability and isolation
β
Upsell opportunity (dedicated workspace as premium feature)
Cons:
β Most complex to manage
β Two code paths to maintain
Use Case: Freemium/tiered SaaS products
Monitoring and Operations
Capacity Monitoring Script
# Monitor Power BI Embedded capacity utilization
function Get-EmbeddedCapacityMetrics {
param(
[string]$ResourceGroup,
[string]$CapacityName,
[int]$LastHours = 24
)
$endTime = Get-Date
$startTime = $endTime.AddHours(-$LastHours)
Write-Host "Fetching capacity metrics: $CapacityName" -ForegroundColor Cyan
# CPU utilization
$cpuMetrics = az monitor metrics list `
--resource "/subscriptions/{subscription-id}/resourceGroups/$ResourceGroup/providers/Microsoft.PowerBIDedicated/capacities/$CapacityName" `
--metric "cpu_metric" `
--start-time $startTime.ToString("yyyy-MM-ddTHH:mm:ss") `
--end-time $endTime.ToString("yyyy-MM-ddTHH:mm:ss") `
--interval PT1H `
--aggregation Average `
--output json | ConvertFrom-Json
# Memory utilization
$memoryMetrics = az monitor metrics list `
--resource "/subscriptions/{subscription-id}/resourceGroups/$ResourceGroup/providers/Microsoft.PowerBIDedicated/capacities/$CapacityName" `
--metric "memory_metric" `
--start-time $startTime.ToString("yyyy-MM-ddTHH:mm:ss") `
--end-time $endTime.ToString("yyyy-MM-ddTHH:mm:ss") `
--interval PT1H `
--aggregation Average `
--output json | ConvertFrom-Json
# Calculate statistics
$avgCPU = ($cpuMetrics.value.timeseries.data.average | Measure-Object -Average).Average
$maxCPU = ($cpuMetrics.value.timeseries.data.average | Measure-Object -Maximum).Maximum
$avgMemory = ($memoryMetrics.value.timeseries.data.average | Measure-Object -Average).Average
$maxMemory = ($memoryMetrics.value.timeseries.data.average | Measure-Object -Maximum).Maximum
Write-Host "`n=== Capacity Metrics (Last $LastHours hours) ===" -ForegroundColor Yellow
Write-Host "CPU Utilization:"
Write-Host " Average: $([math]::Round($avgCPU, 2))%"
Write-Host " Maximum: $([math]::Round($maxCPU, 2))%"
if ($maxCPU -gt 80) {
Write-Host " β οΈ WARNING: CPU usage exceeded 80%" -ForegroundColor Red
}
Write-Host "`nMemory Utilization:"
Write-Host " Average: $([math]::Round($avgMemory, 2))%"
Write-Host " Maximum: $([math]::Round($maxMemory, 2))%"
if ($maxMemory -gt 80) {
Write-Host " β οΈ WARNING: Memory usage exceeded 80%" -ForegroundColor Red
}
# Recommendations
Write-Host "`n=== Recommendations ===" -ForegroundColor Cyan
if ($avgCPU -gt 70) {
Write-Host " β’ Consider scaling up to larger SKU"
} elseif ($avgCPU -lt 30 -and $maxCPU -lt 50) {
Write-Host " β’ Consider scaling down to save costs"
} else {
Write-Host " β’ Capacity is appropriately sized"
}
}
# Run monitoring
Get-EmbeddedCapacityMetrics -ResourceGroup "rg-powerbi-embedded" -CapacityName "pbiembed-prod-001"
Application Insights Integration
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DataContracts;
public class EmbeddedAnalyticsLogger
{
private readonly TelemetryClient _telemetry;
public EmbeddedAnalyticsLogger(TelemetryClient telemetry)
{
_telemetry = telemetry;
}
public void LogEmbedTokenGenerated(string reportName, string tenantId, string userId)
{
var telemetry = new EventTelemetry("EmbedTokenGenerated");
telemetry.Properties["ReportName"] = reportName;
telemetry.Properties["TenantId"] = tenantId;
telemetry.Properties["UserId"] = userId;
telemetry.Properties["Timestamp"] = DateTime.UtcNow.ToString("o");
_telemetry.TrackEvent(telemetry);
}
public void LogReportLoadTime(string reportName, double loadTimeMs)
{
_telemetry.TrackMetric("ReportLoadTime", loadTimeMs, new Dictionary<string, string>
{
{ "ReportName", reportName }
});
}
public void LogEmbedError(string reportName, Exception ex, string userId)
{
_telemetry.TrackException(ex, new Dictionary<string, string>
{
{ "ReportName", reportName },
{ "UserId", userId },
{ "ErrorType", "EmbedFailure" }
});
}
}
Security Best Practices
β Token Management:
β Generate tokens server-side only (never client-side)
β Use minimum token lifetime needed (default: 60 min)
β Implement token refresh before expiry
β Never log or expose embed tokens
β Rotate client secrets every 90 days
β Service Principal:
β Use dedicated service principal per environment
β Store credentials in Azure Key Vault
β Grant minimum required permissions
β Monitor service principal activity
β Use managed identity where possible
β Row-Level Security:
β Always implement RLS for multi-tenant scenarios
β Test RLS with each tenant identity
β Use dynamic RLS based on USERNAME()
β Audit RLS rules quarterly
β Document RLS logic clearly
β Network Security:
β Use HTTPS only for all API calls
β Implement CORS policies restrictively
β Whitelist Power BI domains in CSP headers
β Enable Azure Private Link (for Premium)
β Monitor unusual access patterns
β Application Security:
β Authenticate users in your application first
β Validate user permissions before generating tokens
β Implement rate limiting on embed token endpoints
β Log all embed token requests
β Alert on failed authorization attempts
Troubleshooting Guide
Issue 1: "Failed to get embed token - 401 Unauthorized"
Diagnosis:
# Test service principal authentication
$tenantId = "your-tenant-id"
$clientId = "your-client-id"
$clientSecret = "your-client-secret"
$body = @{
grant_type = "client_credentials"
client_id = $clientId
client_secret = $clientSecret
resource = "https://analysis.windows.net/powerbi/api"
}
$response = Invoke-RestMethod -Method Post -Uri "https://login.microsoftonline.com/$tenantId/oauth2/token" -Body $body
if ($response.access_token) {
Write-Host "β
Authentication successful" -ForegroundColor Green
} else {
Write-Host "β Authentication failed" -ForegroundColor Red
}
Common Causes:
- Service principal not enabled in Power BI tenant settings
- Client secret expired
- Service principal doesn't have workspace access
- Incorrect tenant ID
Resolution:
- Verify tenant settings enable service principals
- Generate new client secret if expired
- Grant workspace Member/Admin role to service principal
- Double-check tenant ID in code
Issue 2: "Report fails to load - blank iframe"
Diagnosis:
// Check browser console for errors
// Common errors:
// - CORS policy blocking
// - Content Security Policy (CSP) violation
// - Invalid embed token
// - Workspace not assigned to capacity
Resolutions:
// 1. Verify CORS headers on backend API
app.UseCors(policy => policy
.WithOrigins("https://your-app.com")
.AllowAnyMethod()
.AllowAnyHeader());
// 2. Add Power BI domains to CSP headers
// In HTML or HTTP response header:
<meta http-equiv="Content-Security-Policy"
content="frame-src 'self' https://app.powerbi.com https://app.fabric.microsoft.com;">
// 3. Check workspace capacity assignment
// Power BI Service β Workspace Settings β Premium β Verify capacity assigned
Issue 3: RLS not filtering data correctly
Diagnosis:
// Test RLS in Power BI Desktop
// Home β View As Roles β Select "TenantUser" β Enter username
// Check USERNAME() function result:
UsernameTest = USERNAME()
// Verify RLS expression:
[TenantID] = RIGHT(USERNAME(), LEN(USERNAME()) - FIND(":", USERNAME()))
Common Issues:
- USERNAME() format doesn't match expectation
- RLS role not selected during token generation
- Multiple RLS rules conflicting
- Case sensitivity in comparisons
Resolution:
- Standardize username format (e.g., "tenant:12345")
- Always specify roles in GenerateTokenRequest
- Use UPPER() or LOWER() for case-insensitive comparisons
- Test RLS with each tenant's actual data
Best Practices Summary
β Architecture:
β Use service principal authentication (not master user)
β Implement workspace-per-tenant OR shared workspace with RLS
β Assign workspaces to Embedded capacity
β Separate dev/test/prod capacities
β Performance:
β Enable auto-pause for non-24/7 workloads
β Monitor capacity utilization daily
β Cache embed tokens (60 sec) for repeated accesses
β Optimize report queries and visuals
β Use incremental refresh for large datasets
β Security:
β Generate tokens server-side only
β Implement RLS for multi-tenancy
β Rotate client secrets quarterly
β Store secrets in Azure Key Vault
β Monitor unauthorized access attempts
β Operations:
β Integrate with Application Insights
β Alert on capacity >80% sustained
β Automate capacity scaling
β Document disaster recovery procedures
β Test token refresh logic thoroughly
β User Experience:
β Show loading skeleton while fetching token
β Handle token expiry gracefully
β Implement error boundaries in React
β Provide fallback for slow connections
β White-label completely (no Power BI branding)
Key Takeaways
- Service Principal authentication is the production-standard approach (not master user)
- Capacity planning requires understanding concurrent users, reports, and query patterns
- Multi-tenancy strategy choice (workspace-per-tenant vs. RLS) depends on scale and isolation requirements
- Token management must handle generation, refresh, and expiry gracefully
- Frontend integration requires powerbi-client.js library and proper event handling
- Monitoring capacity utilization prevents performance issues and cost overruns
- Security layers include Azure AD, service principal permissions, RLS, and token lifetime limits
- Auto-pause can save 70%+ on costs for non-24/7 workloads
- Testing RLS is criticalβalways validate with actual tenant identities
- Documentation of tenant mappings, RLS logic, and deployment procedures prevents operational issues
Next Steps
- Provision Azure resources (capacity, service principal, Key Vault)
- Enable service principals in Power BI tenant settings
- Implement backend service for token generation (.NET/Node.js)
- Build frontend component with powerbi-client.js (React/Angular/Vue)
- Configure RLS for multi-tenant data isolation
- Set up monitoring (Application Insights, capacity alerts)
- Test thoroughly (token refresh, RLS, error handling, performance)
- Document architecture (tenant mapping, security model, runbooks)
- Deploy to production with gradual rollout
- Monitor and optimize (capacity utilization, user experience, costs)
Additional Resources
- Power BI Embedded Documentation
- Power BI REST API Reference
- Power BI JavaScript API
- Embed Token Generation
- Row-Level Security
- Capacity Planning
- Multi-Tenancy Patterns
- Power BI Embedded Pricing
Embed intelligence. Scale seamlessly. Secure completely.