Embedding Migration: Tableau Embedded to Power BI Embedded¶
Comparative positioning note
This document is written from the perspective of Microsoft Azure, Cloud Scale Analytics, and CSA Loom. Any description of third-party or competing products, services, pricing, or capabilities is derived from publicly available documentation and sources believed accurate at the time of writing, and is provided for general comparison only. We do not claim expertise in, or authority over, any non-Microsoft product or service; the respective vendor's official documentation is the authoritative source for their offerings, which may change over time. Nothing here is intended to disparage any vendor — where a competing product has genuine advantages, we aim to note them honestly. Verify all third-party details against the vendor's current official documentation before making decisions.
A comprehensive guide for migrating embedded analytics from Tableau Embedded Analytics to Power BI Embedded, covering pricing, SDK, architecture, multi-tenancy, and white-labeling.
Overview¶
Embedded analytics — the ability to embed interactive reports inside customer-facing applications, partner portals, and internal tools — is where the cost difference between Tableau and Power BI is most dramatic. Tableau uses per-user pricing for embedded scenarios. Power BI Embedded uses capacity-based pricing that serves unlimited users within a fixed compute budget. This guide covers the full migration path.
1. Architecture comparison¶
1.1 High-level architecture¶
flowchart LR
subgraph Tableau["Tableau Embedded"]
TAB_SRV[Tableau Server/Cloud<br/>Per-user licensing] --> TAB_API[JavaScript API v3<br/>Embed via iframe]
TAB_API --> TAB_APP[Web Application<br/>Tableau session per user]
end
subgraph PowerBI["Power BI Embedded"]
PBI_SVC[Power BI Service<br/>Fabric capacity] --> PBI_API[Power BI JS SDK<br/>Embed via iframe]
PBI_API --> PBI_APP[Web Application<br/>Embed token per session]
PBI_SVC --> PBI_TOKEN[Token Service<br/>Service principal auth]
end 1.2 Key architectural differences¶
| Aspect | Tableau Embedded | Power BI Embedded | Impact |
|---|---|---|---|
| Pricing model | Per-user (Viewer: $15/user/mo) | Capacity-based (F-SKU: fixed monthly) | Power BI scales to unlimited users at fixed cost |
| Authentication | Tableau Server session (SAML/OIDC) | Embed token (service principal) or AAD | Power BI supports app-owns-data (no user auth needed) |
| Embedding method | JavaScript API v3 + iframe | Power BI JS SDK + iframe | Similar approach; different SDK |
| Multi-tenancy | Row-level security or separate sites | RLS + workspace isolation | Power BI provides more flexible isolation options |
| White-labeling | Limited (custom CSS, hide toolbar) | Full branding control (custom theme, no PBI chrome) | Power BI allows complete brand customization |
| Performance | Depends on Tableau Server capacity | Depends on Fabric capacity SKU | Both scale with allocated compute |
2. Pricing comparison¶
2.1 Tableau Embedded pricing¶
Tableau Embedded Analytics requires Viewer licenses for every external user who views embedded content:
| External users | License tier | Monthly cost | Annual cost |
|---|---|---|---|
| 100 | Viewer ($15/user) | $1,500 | $18,000 |
| 500 | Viewer | $7,500 | $90,000 |
| 1,000 | Viewer | $15,000 | $180,000 |
| 5,000 | Viewer | $75,000 | $900,000 |
| 10,000 | Viewer | $150,000 | $1,800,000 |
| 50,000 | Viewer | $750,000 | $9,000,000 |
Infrastructure costs (Tableau Server VMs to handle embedded traffic) add 20-40% to these numbers.
2.2 Power BI Embedded pricing¶
Power BI Embedded uses Fabric capacity SKUs. You pay for compute, not users:
| F-SKU | Monthly cost | Concurrent users (typical) | Annual cost |
|---|---|---|---|
| F2 | ~$262 | 5-20 | ~$3,150 |
| F4 | ~$524 | 10-50 | ~$6,290 |
| F8 | ~$1,050 | 25-100 | ~$12,600 |
| F16 | ~$2,100 | 50-250 | ~$25,200 |
| F32 | ~$3,000 | 100-500 | ~$36,000 |
| F64 | ~$5,000 | 250-1,000 | ~$60,000 |
| F128 | ~$10,000 | 500-2,500 | ~$120,000 |
| F256 | ~$20,000 | 1,000-5,000 | ~$240,000 |
Right-sizing capacity
Concurrent users depend on report complexity, data volume, and query patterns. Start with F32 or F64, monitor with Capacity Metrics App, and scale as needed. Auto-scale is available for burst scenarios.
2.3 Cost comparison at scale¶
| External users | Tableau annual | Power BI annual | Savings |
|---|---|---|---|
| 500 | $90,000+ | ~$36,000 (F32) | 60% |
| 2,000 | $360,000+ | ~$60,000 (F64) | 83% |
| 10,000 | $1,800,000+ | ~$120,000 (F128) | 93% |
| 50,000 | $9,000,000+ | ~$240,000 (F256) | 97% |
3. Embedding approaches¶
3.1 Tableau JavaScript API to Power BI JavaScript SDK¶
| Tableau JS API concept | Power BI JS SDK concept | Notes |
|---|---|---|
tableau.Viz(container, url, options) | powerbi.embed(container, config) | Initialization pattern |
| Viz URL | Embed URL + embed token | Power BI uses token-based auth |
viz.getWorkbook() | Report object | Access report object for interaction |
viz.getActiveSheet() | Report pages | report.getPages() |
sheet.applyFilterAsync() | page.setFilters() or report.setFilters() | Apply filters programmatically |
sheet.selectMarksAsync() | Visual interaction via SDK | Click and selection events |
sheet.getSelectedMarksAsync() | Data export via SDK | Extract selected data |
viz.addEventListener() | report.on('event', callback) | Event handling |
viz.dispose() | powerbi.reset(container) | Cleanup |
3.2 Tableau embed code example¶
<!-- Tableau Embedded -->
<div id="tableauViz"></div>
<script>
var containerDiv = document.getElementById("tableauViz");
var url = "https://tableau-server/views/Dashboard/Sheet1";
var options = {
hideTabs: true,
hideToolbar: true,
width: "100%",
height: "600px",
onFirstInteractive: function () {
console.log("Dashboard loaded");
},
};
var viz = new tableau.Viz(containerDiv, url, options);
</script>
3.3 Power BI embed code equivalent¶
<!-- Power BI Embedded -->
<div id="reportContainer"></div>
<script src="https://cdn.jsdelivr.net/npm/powerbi-client/dist/powerbi.min.js"></script>
<script>
var models = window["powerbi-client"].models;
var embedConfig = {
type: "report",
id: "report-id-guid",
embedUrl: "https://app.powerbi.com/reportEmbed?reportId=...",
accessToken: "embed-token-from-backend",
tokenType: models.TokenType.Embed,
settings: {
navContentPaneEnabled: false,
filterPaneEnabled: false,
layoutType: models.LayoutType.Custom,
customLayout: {
displayOption: models.DisplayOption.FitToWidth,
},
},
};
var reportContainer = document.getElementById("reportContainer");
var report = powerbi.embed(reportContainer, embedConfig);
report.on("loaded", function () {
console.log("Report loaded");
});
report.on("dataSelected", function (event) {
console.log("Data selected:", event.detail);
});
</script>
3.4 SDK feature comparison¶
| Capability | Tableau JS API | Power BI JS SDK | Notes |
|---|---|---|---|
| Embed report | Yes | Yes | Core capability |
| Apply filters | Yes | Yes | Similar API patterns |
| Get/set parameters | Yes | Yes (slicers) | Power BI uses slicer state instead of parameters |
| Event handling | Yes | Yes | Load, render, click, select events |
| Export data | Yes | Yes | Export visual or underlying data |
| Export report (PDF) | Limited | Yes | report.print() or report.export() |
| Theme/branding | Limited CSS | Full theme JSON + custom layout | Power BI provides more control |
| Page navigation | Yes | Yes | Navigate between report pages |
| Bookmarks | N/A | Yes | Apply bookmarks programmatically |
| Full-screen mode | Yes | Yes | Toggle full-screen |
| Responsive/mobile layout | Limited | Yes | Power BI supports mobile layout embed |
| Q&A (natural language) | N/A | Yes | Embed a Q&A visual for natural language queries |
| Dashboard embed | Yes | Yes | Pin tiles from multiple reports |
| Single visual embed | N/A | Yes | Embed individual visuals, not full reports |
4. Authentication and security¶
4.1 Authentication model comparison¶
| Pattern | Tableau | Power BI | Description |
|---|---|---|---|
| User owns data | SAML/OIDC SSO | Entra ID SSO | User authenticates directly; sees their own data |
| App owns data | Trusted tickets | Service principal + embed token | App authenticates on behalf of user; token-based access |
| Anonymous embed | Publish to web (limited) | Publish to web (limited) | Public embed without authentication |
4.2 App-owns-data pattern (most common for embedding)¶
// Token generation flow (backend):
// 1. App authenticates to Entra ID using service principal
// 2. App calls Power BI REST API to generate embed token
// 3. Embed token is passed to the frontend
// 4. Frontend uses embed token to render the report
// 5. RLS identity is included in the token to filter data
// Backend (Node.js example):
const { ConfidentialClientApplication } = require("@azure/msal-node");
const axios = require("axios");
// Step 1: Get AAD token
const msalConfig = {
auth: {
clientId: "service-principal-client-id",
clientSecret: "client-secret",
authority: "https://login.microsoftonline.com/tenant-id"
}
};
const cca = new ConfidentialClientApplication(msalConfig);
const tokenResponse = await cca.acquireTokenByClientCredential({
scopes: ["https://analysis.windows.net/powerbi/api/.default"]
});
// Step 2: Generate embed token with RLS
const embedTokenRequest = {
datasets: [{ id: "dataset-id" }],
reports: [{ id: "report-id" }],
identities: [{
username: "user@company.com",
roles: ["RegionFilter"],
datasets: ["dataset-id"]
}]
};
const embedToken = await axios.post(
"https://api.powerbi.com/v1.0/myorg/GenerateToken",
embedTokenRequest,
{ headers: { Authorization: `Bearer ${tokenResponse.accessToken}` } }
);
5. Multi-tenancy patterns¶
5.1 Comparison of isolation approaches¶
| Pattern | Tableau | Power BI | Best for |
|---|---|---|---|
| Shared model + RLS | User filters on data source | RLS roles with dynamic identity | Cost-efficient; shared infrastructure |
| Workspace per tenant | Site per tenant (expensive) | Workspace per tenant | Strong isolation; tenant-specific data models |
| Database per tenant | Separate data source per tenant | Separate semantic model or data source per tenant | Maximum isolation; regulatory requirements |
5.2 RLS-based multi-tenancy (recommended)¶
// Single semantic model serves all tenants
// RLS role: TenantFilter
// Table: Sales
// DAX expression:
[TenantID] = USERNAME()
// or
LOOKUPVALUE(
TenantMapping[TenantID],
TenantMapping[UserEmail], USERPRINCIPALNAME()
) = Sales[TenantID]
// Embed token includes effective identity:
{
"identities": [{
"username": "tenant-123",
"roles": ["TenantFilter"],
"datasets": ["dataset-id"]
}]
}
5.3 Workspace-based multi-tenancy¶
For stronger isolation, create a workspace per tenant with a dedicated semantic model:
Workspace: "Embedded - Tenant A"
- Tenant A Semantic Model (filtered to Tenant A data)
- Tenant A Report
Workspace: "Embedded - Tenant B"
- Tenant B Semantic Model (filtered to Tenant B data)
- Tenant B Report
This provides complete data isolation but increases management overhead.
6. White-labeling and branding¶
6.1 Tableau branding limitations¶
Tableau offers limited branding for embedded scenarios:
- Custom logo on Tableau Server login page
- CSS overrides (limited, can break on upgrades)
- Hide toolbar and tabs
- Custom URL (via reverse proxy)
6.2 Power BI branding capabilities¶
Power BI Embedded provides comprehensive branding control:
| Branding feature | How to implement |
|---|---|
| Custom colors and fonts | Apply a Power BI theme JSON |
| Remove Power BI chrome | Set navContentPaneEnabled: false and filterPaneEnabled: false |
| Custom background | Theme JSON background setting |
| No "Power BI" branding | A-SKU or F-SKU Embedded licenses remove the footer |
| Custom loading animation | CSS styling on the container element |
| Custom error messages | Handle errors in the JavaScript SDK event handler |
| Responsive layout | Use DisplayOption.FitToWidth or FitToPage |
| Custom toolbar | Build your own toolbar using SDK methods (export, print, refresh) |
| Localization | Power BI supports localization for 40+ languages |
6.3 Theme JSON for branding¶
{
"name": "Customer Portal Theme",
"dataColors": ["#0063B1", "#2D7D9A", "#77B900", "#FF8C00", "#E81123"],
"background": "#F5F5F5",
"foreground": "#333333",
"tableAccent": "#0063B1",
"good": "#77B900",
"neutral": "#FFB900",
"bad": "#E81123",
"visualStyles": {
"*": {
"*": {
"title": [
{
"fontFamily": "Your Brand Font",
"fontSize": 14,
"color": { "solid": { "color": "#0063B1" } }
}
]
}
}
}
}
7. Performance optimization¶
7.1 Embedded performance best practices¶
| Technique | Description | Impact |
|---|---|---|
| Pre-load reports | Embed with { type: "report", id: "..." } config before user navigates | Eliminates load time when user switches to analytics tab |
| Bookmark for default state | Apply a bookmark immediately after load | Faster initial view than applying filters post-load |
| Reduce visual count | Keep embedded pages to 6-8 visuals max | Each visual generates queries; fewer = faster |
| Use Import mode | Import mode is faster than DirectQuery for most embedded scenarios | Eliminates source query latency |
| Right-size capacity | Monitor CU usage; upgrade if throttling occurs | Prevents slow render under load |
| Token caching | Cache embed tokens (valid for 60 minutes by default) | Reduces backend token generation calls |
| Client-side caching | Use powerbi.bootstrap(container, config) for pre-initialization | Report container ready before data arrives |
7.2 Capacity sizing for embedded¶
| Concurrent report views | Recommended starting SKU | Notes |
|---|---|---|
| 1-25 | F8 | Light workloads, simple reports |
| 25-100 | F16-F32 | Medium complexity, moderate concurrency |
| 100-500 | F64 | Standard production embedding |
| 500-2,000 | F128-F256 | High-traffic portals |
| 2,000+ | F256+ or auto-scale | Enterprise-scale embedding |
8. Migration steps¶
8.1 Migration execution plan¶
- Inventory embedded Tableau content — list all embedded dashboards, their applications, and user counts
- Set up Power BI Embedded capacity — provision an F-SKU in Azure portal
- Register a service principal — create an Entra ID app registration for the embedding application
- Migrate reports — convert Tableau workbooks to Power BI reports (see Tutorial: Workbook to PBIX)
- Publish to embedding workspace — publish reports to a dedicated workspace
- Implement token generation — build backend service to generate embed tokens
- Update frontend — replace Tableau JS API with Power BI JS SDK
- Configure RLS — implement row-level security for multi-tenancy
- Apply branding — create and apply Power BI theme
- Performance test — load test with expected concurrent users; right-size capacity
- Cut over — switch the application from Tableau to Power BI
- Decommission — remove Tableau Viewer licenses
8.2 Testing checklist¶
- Reports render correctly in the embedding container
- RLS filters data correctly for each tenant/user
- Filters applied programmatically work as expected
- Events (loaded, rendered, dataSelected) fire correctly
- Export (PDF, data) works from embedded context
- Mobile/responsive layout renders correctly
- Performance under load meets SLA (< 3 second render time)
- Token refresh works (tokens expire after 60 minutes by default)
- White-labeling matches brand guidelines
- Error handling displays custom error messages
Last updated: 2026-04-30 Maintainers: CSA-in-a-Box core team Related: TCO Analysis | Server Migration | Migration Playbook