Back to Blog
4 min read

Power BI Embed for Customers: Building Analytics Portals

Embed for customers (App Owns Data) enables you to embed Power BI content for external users who don’t have Power BI accounts. It’s perfect for customer portals, SaaS products, and partner dashboards.

Architecture

┌─────────────────────────────────────────────────────────┐
│                   Your Application                       │
│                                                         │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐ │
│  │   Frontend  │    │   Backend   │    │  Power BI   │ │
│  │  (React/    │◄──►│   (API)     │◄──►│   Service   │ │
│  │   Angular)  │    │             │    │             │ │
│  └─────────────┘    └─────────────┘    └─────────────┘ │
│         │                  │                  │         │
│         │                  │                  │         │
│         ▼                  ▼                  ▼         │
│  ┌─────────────────────────────────────────────────┐   │
│  │              Embedded Capacity (A1-A6)          │   │
│  └─────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

Service Principal Setup

# Create service principal
az ad sp create-for-rbac \
    --name "PowerBI-Embed-SP" \
    --role "Contributor" \
    --scopes "/subscriptions/{subscription-id}"

# Grant Power BI permissions
# In Azure Portal > Azure AD > App registrations > API permissions
# Add: Power BI Service - All permissions

Workspace Configuration

// Add service principal to workspace as Admin
using Microsoft.PowerBI.Api;

public async Task ConfigureWorkspace(string workspaceId, string servicePrincipalId)
{
    var groupUser = new GroupUser
    {
        Identifier = servicePrincipalId,
        PrincipalType = PrincipalType.App,
        GroupUserAccessRight = GroupUserAccessRight.Admin
    };

    await _client.Groups.AddGroupUserAsync(
        Guid.Parse(workspaceId),
        groupUser
    );
}

Multi-Tenant Implementation

Per-Customer Workspaces

public class MultiTenantEmbedService
{
    private readonly PowerBIClient _client;

    public async Task<string> ProvisionTenantWorkspace(string tenantId, string tenantName)
    {
        // Create workspace for tenant
        var workspace = await _client.Groups.CreateGroupAsync(
            new GroupCreationRequest
            {
                Name = $"Customer-{tenantId}-{tenantName}"
            }
        );

        // Clone template reports
        await CloneTemplateReports(workspace.Id, tenantId);

        // Set up dataset with tenant connection
        await ConfigureTenantDataset(workspace.Id, tenantId);

        return workspace.Id.ToString();
    }

    private async Task CloneTemplateReports(Guid workspaceId, string tenantId)
    {
        var templateWorkspaceId = _config["PowerBI:TemplateWorkspaceId"];
        var templates = await _client.Reports.GetReportsInGroupAsync(
            Guid.Parse(templateWorkspaceId)
        );

        foreach (var template in templates.Value)
        {
            await _client.Reports.CloneReportInGroupAsync(
                Guid.Parse(templateWorkspaceId),
                template.Id,
                new CloneReportRequest
                {
                    Name = template.Name,
                    TargetWorkspaceId = workspaceId
                }
            );
        }
    }
}

Row-Level Security Approach

public async Task<EmbedConfig> GetTenantEmbedConfig(string tenantId, string userId)
{
    var report = await GetReportForTenant(tenantId);

    var tokenRequest = new GenerateTokenRequestV2
    {
        Reports = new List<GenerateTokenRequestV2Report>
        {
            new GenerateTokenRequestV2Report(report.Id)
        },
        Datasets = new List<GenerateTokenRequestV2Dataset>
        {
            new GenerateTokenRequestV2Dataset(report.DatasetId)
        },
        Identities = new List<EffectiveIdentity>
        {
            new EffectiveIdentity(
                username: userId,
                datasets: new List<string> { report.DatasetId },
                roles: new List<string> { "TenantFilter" },
                customData: tenantId  // Pass tenant ID for RLS
            )
        }
    };

    var token = await _client.EmbedToken.GenerateTokenAsync(tokenRequest);

    return new EmbedConfig
    {
        ReportId = report.Id.ToString(),
        EmbedUrl = report.EmbedUrl,
        Token = token.Token,
        Expiration = token.Expiration
    };
}

RLS DAX Expression

// In Power BI Desktop, define RLS role "TenantFilter"
[TenantId] = CUSTOMDATA()

// Or for multiple conditions
[TenantId] = CUSTOMDATA()
OR
[IsPublic] = TRUE

Token Management

public class TokenManager
{
    private readonly IMemoryCache _cache;
    private readonly IEmbedService _embedService;

    public async Task<string> GetOrRefreshToken(string cacheKey, Func<Task<EmbedToken>> generateToken)
    {
        if (_cache.TryGetValue(cacheKey, out TokenCacheEntry cached))
        {
            // Return cached if not expiring soon
            if (cached.Expiration > DateTime.UtcNow.AddMinutes(5))
            {
                return cached.Token;
            }
        }

        // Generate new token
        var newToken = await generateToken();

        _cache.Set(cacheKey, new TokenCacheEntry
        {
            Token = newToken.Token,
            Expiration = newToken.Expiration.Value
        }, newToken.Expiration.Value - DateTime.UtcNow);

        return newToken.Token;
    }
}

Frontend Integration (React)

import { PowerBIEmbed } from 'powerbi-client-react';
import { models } from 'powerbi-client';

function CustomerDashboard({ tenantId }) {
    const [embedConfig, setEmbedConfig] = useState(null);

    useEffect(() => {
        async function loadConfig() {
            const response = await fetch(`/api/embed/${tenantId}`);
            const config = await response.json();
            setEmbedConfig(config);
        }
        loadConfig();
    }, [tenantId]);

    if (!embedConfig) return <div>Loading...</div>;

    return (
        <PowerBIEmbed
            embedConfig={{
                type: 'report',
                id: embedConfig.reportId,
                embedUrl: embedConfig.embedUrl,
                accessToken: embedConfig.token,
                tokenType: models.TokenType.Embed,
                settings: {
                    panes: {
                        filters: { visible: false },
                        pageNavigation: { visible: true }
                    },
                    background: models.BackgroundType.Transparent
                }
            }}
            cssClassName="report-container"
            getEmbeddedComponent={(report) => {
                // Store report reference for interactions
                window.currentReport = report;
            }}
        />
    );
}

Capacity Planning

capacity_tiers:
  A1:
    cores: 1
    memory: 3GB
    max_concurrent_users: ~50
    cost: ~$1/hour

  A2:
    cores: 2
    memory: 5GB
    max_concurrent_users: ~100
    cost: ~$2/hour

  A4:
    cores: 8
    memory: 25GB
    max_concurrent_users: ~400
    cost: ~$8/hour

considerations:
  - Scale based on concurrent users
  - Consider auto-pause for cost savings
  - Monitor capacity metrics
  - Plan for peak usage

Best Practices

security:
  - Never expose service principal credentials to frontend
  - Use short-lived embed tokens (1 hour max)
  - Implement RLS for data isolation
  - Validate tenant access server-side

performance:
  - Cache embed tokens appropriately
  - Use Premium/Embedded for better performance
  - Optimize report design for embedding
  - Implement lazy loading

scalability:
  - Design for multi-tenancy from start
  - Use parameterized datasets where possible
  - Consider workspace per tenant for isolation
  - Monitor and scale capacity proactively

Conclusion

Embed for customers enables powerful analytics in customer-facing applications:

  • No Power BI licenses required for end users
  • Complete control over user experience
  • Secure multi-tenant data access
  • Scalable capacity-based pricing

Resources

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.