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