Embedded Analytics: Power BI Embedded Implementation

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:

  1. Service principal not enabled in Power BI tenant settings
  2. Client secret expired
  3. Service principal doesn't have workspace access
  4. 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

  1. Provision Azure resources (capacity, service principal, Key Vault)
  2. Enable service principals in Power BI tenant settings
  3. Implement backend service for token generation (.NET/Node.js)
  4. Build frontend component with powerbi-client.js (React/Angular/Vue)
  5. Configure RLS for multi-tenant data isolation
  6. Set up monitoring (Application Insights, capacity alerts)
  7. Test thoroughly (token refresh, RLS, error handling, performance)
  8. Document architecture (tenant mapping, security model, runbooks)
  9. Deploy to production with gradual rollout
  10. Monitor and optimize (capacity utilization, user experience, costs)

Additional Resources


Embed intelligence. Scale seamlessly. Secure completely.