To embed Power BI reports in a V0-generated Next.js app, create an API route that authenticates with Azure Active Directory using client credentials, fetches an embed token from the Power BI REST API, and returns it to a client component that renders the report using the powerbi-client library. Store your Azure AD credentials in Vercel environment variables — they never reach the browser.
Embed Power BI Reports and Dashboards in V0-Generated Next.js Apps
Power BI Embedded allows you to display fully interactive Power BI reports inside your own applications without requiring users to have Power BI licenses. For founders and teams building internal tools with V0, this means you can take existing Power BI dashboards — sales reports, operations metrics, financial summaries — and embed them directly in a custom Next.js app with your own branding, navigation, and access controls.
The integration requires three moving pieces. First, an Azure Active Directory app registration that grants your Next.js server permission to call the Power BI REST API as a service principal. Second, a Next.js API route that uses those Azure AD credentials to fetch a short-lived embed token (valid for 60 minutes) from the Power BI API. Third, a client component in your V0-generated app that passes the embed token to the powerbi-client JavaScript library, which renders the report in a secure iframe. This architecture keeps your Azure credentials server-only while giving the browser the minimum information needed to display the report.
For V0-generated apps, the most common Power BI integration scenario is an executive or operations dashboard where different report pages are accessible from a sidebar, and the embedded report responds to filter selections made in the Next.js UI. Power BI's JavaScript SDK supports programmatic filter application, page navigation, and event handling for clicks within the report — all of which can be wired to your V0-generated controls for a seamless experience.
Integration method
Power BI integrates with V0-generated Next.js apps through the Power BI Embedded API and Azure Active Directory authentication. A Next.js API route authenticates with Azure AD using client credentials (tenant ID, client ID, client secret), calls the Power BI REST API to generate an embed token, and returns that short-lived token to the browser. A client component then uses the powerbi-client JavaScript library to render the report in an iframe using the embed token. Your Azure AD credentials never leave the server, and the embed token expires after 60 minutes, limiting the exposure window.
Prerequisites
- A Microsoft Azure account with permissions to create app registrations — create one at portal.azure.com (Azure AD is free, but Power BI Embedded capacity is paid)
- A Power BI Pro or Premium account with at least one published report in a Power BI workspace — reports must be published to a workspace (not My Workspace) for embedding
- An Azure AD app registration with Power BI service principal — follow Microsoft's documentation to register an app and grant it access to Power BI workspaces
- Your Power BI workspace ID and report ID — found in the Power BI report URL (app.powerbi.com/groups/{workspace_id}/reports/{report_id})
- A V0 account at v0.dev and a Vercel account for deployment
Step-by-step guide
Register an Azure AD App and Configure Power BI Access
Register an Azure AD App and Configure Power BI Access
Before writing any code, you need to set up the Azure AD service principal that your Next.js API route will use to authenticate with Power BI. Navigate to portal.azure.com → Azure Active Directory → App registrations → New registration. Give your app a name (e.g., 'MyApp Power BI Embed'), select 'Accounts in this organizational directory only' as the supported account type, and click Register. After registration, note the Application (client) ID and Directory (tenant) ID from the app's Overview page — you will need both for environment variables. Under Certificates & secrets → Client secrets → New client secret, create a secret with a 24-month expiry and copy the secret value immediately (it won't be shown again). Next, grant the app access to the Power BI API: go to API permissions → Add a permission → APIs my organization uses → search 'Power BI Service' → Delegated permissions → add 'Report.ReadAll' and 'Dataset.ReadAll'. Click Grant admin consent. Finally, in Power BI at app.powerbi.com, navigate to the workspace containing your reports → Workspace settings → Access → add your Azure AD app as an Admin or Contributor. This workspace-level permission is required for the service principal to fetch embed tokens for reports in that workspace. The setup process takes approximately 15-20 minutes — this is the most time-consuming part of the Power BI integration, but it only needs to be done once.
Create a Power BI report container component with a full-width iframe area and a loading state. The component accepts props: embedUrl (string), embedToken (string), reportId (string), and type ('report' | 'dashboard'). When props are provided, initialize the Power BI client using window.powerbi.embed() and render the report in the ref element. Show a loading spinner while the report initializes. Show an error state if embedUrl or embedToken is missing, with a message 'Power BI report unavailable — please contact your administrator'. The container should have a minimum height of 600px and be fully responsive.
Paste this in V0 chat
Pro tip: Grant admin consent for API permissions immediately after adding them — without admin consent, the service principal cannot call the Power BI API even with the correct permissions listed. The 'Grant admin consent' button is in the API permissions panel and requires an Azure AD admin account.
Expected result: An Azure AD app is registered with the tenant ID, client ID, and client secret noted. The Power BI workspace shows the app as a member. The app has granted admin consent for Power BI API permissions.
Create the Power BI Embed Token API Route
Create the Power BI Embed Token API Route
Create a Next.js API route that authenticates with Azure AD and fetches a Power BI embed token. The embed token is a short-lived credential (60 minutes by default) that the browser uses to authenticate with Power BI's rendering infrastructure — it grants access to a specific report without exposing your Azure AD credentials. The authentication flow uses the OAuth 2.0 client credentials grant: POST to https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token with your client ID, client secret, and the scope https://analysis.windows.net/powerbi/api/.default. This returns an access token. Use the access token to call the Power BI REST API endpoint POST https://api.powerbi.com/v1.0/myorg/groups/{workspace_id}/reports/{report_id}/GenerateToken with { accessLevel: 'View' } in the request body. This returns the embed token and the embed URL. Return both to the client component. For row-level security (RLS) — filtering data per user — add identities array to the GenerateToken request body with the username and roles for the embedded user. The embed token includes whatever data access restrictions you specify, making it safe to return to the browser.
1// app/api/powerbi/embed-token/route.ts2import { NextRequest, NextResponse } from 'next/server';34async function getAzureAccessToken(): Promise<string> {5 const tenantId = process.env.AZURE_TENANT_ID;6 const clientId = process.env.AZURE_CLIENT_ID;7 const clientSecret = process.env.AZURE_CLIENT_SECRET;89 if (!tenantId || !clientId || !clientSecret) {10 throw new Error('Azure AD credentials are not configured');11 }1213 const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;1415 const response = await fetch(tokenUrl, {16 method: 'POST',17 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },18 body: new URLSearchParams({19 grant_type: 'client_credentials',20 client_id: clientId,21 client_secret: clientSecret,22 scope: 'https://analysis.windows.net/powerbi/api/.default',23 }),24 });2526 if (!response.ok) {27 const error = await response.json();28 throw new Error(error.error_description || 'Azure AD authentication failed');29 }3031 const data = await response.json();32 return data.access_token;33}3435export async function GET(request: NextRequest) {36 const workspaceId = process.env.POWERBI_WORKSPACE_ID;37 const reportId = request.nextUrl.searchParams.get('reportId') || process.env.POWERBI_REPORT_ID;3839 if (!workspaceId || !reportId) {40 return NextResponse.json(41 { error: 'Power BI workspace or report ID is not configured' },42 { status: 500 }43 );44 }4546 try {47 const accessToken = await getAzureAccessToken();4849 // Get report details (embed URL)50 const reportUrl = `https://api.powerbi.com/v1.0/myorg/groups/${workspaceId}/reports/${reportId}`;51 const reportResponse = await fetch(reportUrl, {52 headers: { Authorization: `Bearer ${accessToken}` },53 });5455 if (!reportResponse.ok) {56 throw new Error(`Failed to fetch report: ${reportResponse.statusText}`);57 }5859 const report = await reportResponse.json();6061 // Generate embed token62 const embedTokenUrl = `https://api.powerbi.com/v1.0/myorg/groups/${workspaceId}/reports/${reportId}/GenerateToken`;63 const tokenResponse = await fetch(embedTokenUrl, {64 method: 'POST',65 headers: {66 Authorization: `Bearer ${accessToken}`,67 'Content-Type': 'application/json',68 },69 body: JSON.stringify({ accessLevel: 'View' }),70 });7172 if (!tokenResponse.ok) {73 throw new Error(`Failed to generate embed token: ${tokenResponse.statusText}`);74 }7576 const tokenData = await tokenResponse.json();7778 return NextResponse.json({79 embedToken: tokenData.token,80 embedUrl: report.embedUrl,81 reportId,82 expiry: tokenData.expiration,83 });84 } catch (error) {85 const message = error instanceof Error ? error.message : 'Failed to generate embed token';86 console.error('Power BI embed error:', message);87 return NextResponse.json({ error: message }, { status: 500 });88 }89}Pro tip: Cache the Azure AD access token in a module-level variable and check its expiry before each request — Azure AD tokens are valid for 60 minutes, so caching them avoids unnecessary authentication roundtrips and reduces latency for each embed token request.
Expected result: GET /api/powerbi/embed-token returns { embedToken, embedUrl, reportId, expiry } which the client component uses to render the Power BI report. The response contains everything the powerbi-client library needs.
Render the Power BI Report with powerbi-client
Render the Power BI Report with powerbi-client
Create a client component that fetches the embed token from your API route and uses the powerbi-client library to render the Power BI report in an iframe. Install the library: npm install powerbi-client. The powerbi-client library uses a service instance (new service.Service()) to manage report embedding. Pass a ref to a div element, the embed token, embed URL, and report ID to the embed() method, and the library handles the iframe creation, authentication, and rendering. The embed configuration requires type: 'report', tokenType: models.TokenType.Embed, accessToken (the embed token), embedUrl, id (the report ID), and settings for toolbar, filter pane, and navigation visibility. The PowerBIEmbed component is the simplest way to use powerbi-client in React — it handles the lifecycle methods for you. Import it from 'powerbi-client-react'. After the report loads, Power BI fires a 'loaded' event that you can listen to for showing/hiding loading states. For applying filters programmatically, use the report.setFilters() method after the report loads. Embed tokens expire after 60 minutes — implement a token refresh mechanism that calls your API route before expiry and calls report.setAccessToken() with the new token to update the session without reloading the report. Handle the expiry in a useEffect that sets a timer to refresh the token 5 minutes before expiry using the expiry timestamp returned by your API route.
Create a PowerBIReportViewer component ('use client') that fetches an embed token from GET /api/powerbi/embed-token on mount. While loading, show a skeleton with a pulsing gray rectangle. On success, render the Power BI report using the embedToken and embedUrl from the response. Add error handling that shows a red error card with the error message if the token fetch fails or the report fails to load. Include a 'Refresh Report' button that re-fetches the embed token. The component should accept a reportId prop that is passed as a query parameter to the API route. Make the report container take 100% width and have a fixed height of 700px with overflow hidden.
Paste this in V0 chat
1// components/PowerBIViewer.tsx2'use client';34import { useEffect, useState, useRef } from 'react';56interface EmbedConfig {7 embedToken: string;8 embedUrl: string;9 reportId: string;10 expiry: string;11}1213interface PowerBIViewerProps {14 reportId?: string;15 height?: number;16}1718export function PowerBIViewer({ reportId, height = 700 }: PowerBIViewerProps) {19 const containerRef = useRef<HTMLDivElement>(null);20 const [config, setConfig] = useState<EmbedConfig | null>(null);21 const [loading, setLoading] = useState(true);22 const [error, setError] = useState<string | null>(null);2324 useEffect(() => {25 const fetchEmbedConfig = async () => {26 setLoading(true);27 setError(null);28 try {29 const params = reportId ? `?reportId=${reportId}` : '';30 const response = await fetch(`/api/powerbi/embed-token${params}`);31 const data = await response.json();32 if (!response.ok) throw new Error(data.error || 'Failed to load report');33 setConfig(data);34 } catch (err) {35 setError(err instanceof Error ? err.message : 'Unknown error');36 } finally {37 setLoading(false);38 }39 };4041 fetchEmbedConfig();42 }, [reportId]);4344 useEffect(() => {45 if (!config || !containerRef.current || typeof window === 'undefined') return;4647 // Dynamically import powerbi-client to avoid SSR issues48 import('powerbi-client').then(({ service, factories, models }) => {49 const powerbi = new service.Service(50 factories.hpmFactory,51 factories.wpmpFactory,52 factories.routerFactory53 );5455 const embedConfig = {56 type: 'report',57 id: config.reportId,58 embedUrl: config.embedUrl,59 accessToken: config.embedToken,60 tokenType: models.TokenType.Embed,61 settings: {62 panes: { filters: { visible: false }, pageNavigation: { visible: true } },63 background: models.BackgroundType.Transparent,64 },65 };6667 powerbi.embed(containerRef.current!, embedConfig);68 });69 }, [config]);7071 if (loading) {72 return (73 <div className="animate-pulse bg-gray-200 rounded-lg" style={{ height }} />74 );75 }7677 if (error) {78 return (79 <div className="flex items-center justify-center bg-red-50 border border-red-200 rounded-lg" style={{ height }}>80 <p className="text-red-600 text-sm">{error}</p>81 </div>82 );83 }8485 return <div ref={containerRef} style={{ height, width: '100%' }} />;86}Pro tip: Use dynamic import for powerbi-client (import('powerbi-client')) to avoid 'window is not defined' errors during Next.js server-side rendering — the library accesses browser APIs and must only run client-side.
Expected result: The PowerBIViewer component renders the Power BI report in a full-width container with a loading skeleton state. The report is interactive — users can click on visualizations and navigate between pages.
Configure Azure Credentials in Vercel and Deploy
Configure Azure Credentials in Vercel and Deploy
Push your code to GitHub and configure Azure AD credentials and Power BI workspace identifiers in Vercel. Navigate to the Vercel Dashboard → your project → Settings → Environment Variables. Add five variables: AZURE_TENANT_ID (the Directory (tenant) ID from your Azure AD app registration Overview — a UUID format like xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx), AZURE_CLIENT_ID (the Application (client) ID from the Azure AD app registration Overview — also UUID format), AZURE_CLIENT_SECRET (the client secret value you copied when creating the secret — a long string; note this is the Value, not the Secret ID), POWERBI_WORKSPACE_ID (the workspace GUID from the Power BI report URL — the first long number after /groups/ in app.powerbi.com/groups/{workspace_id}/reports/), and POWERBI_REPORT_ID (the report GUID from the URL — the number after /reports/ in the URL). None of these should have the NEXT_PUBLIC_ prefix — all are server-side secrets. Set all variables for Production and Preview environments. After saving, trigger a redeployment. Test the deployed app by navigating to a page containing the PowerBIViewer component — the report should render after a brief loading state. If you receive a 'AADSTS70011' Azure AD error, check that the scope in your token request matches exactly (https://analysis.windows.net/powerbi/api/.default) and that admin consent has been granted. If you receive a 403 from the Power BI API, verify the Azure AD app has been added to the Power BI workspace as a Member or Admin — the workspace access is separate from API permissions.
Pro tip: Azure AD client secrets have an expiry date — set a calendar reminder to rotate your AZURE_CLIENT_SECRET before it expires (maximum 24 months). An expired secret causes immediate authentication failures and requires creating a new secret and updating the Vercel environment variable.
Expected result: The deployed Vercel app successfully renders Power BI reports fetched through Azure AD authentication. The embed token is obtained server-side, and the report renders in the browser without exposing Azure credentials.
Common use cases
Executive Dashboard with Embedded Reports
An internal dashboard where leadership can view Power BI sales, operations, and finance reports in a unified Next.js interface with custom navigation, date range pickers that apply filters to the embedded report, and export controls — without leaving your product.
Create an executive dashboard with a left sidebar listing four report sections: Sales Overview, Operations Metrics, Financial Summary, and Customer Analytics. Each section shows an icon and a report title. The main content area shows the selected Power BI report embedded in an iframe (the embed URL and token come from /api/powerbi/embed-token). Add a date range picker in the top right that sends filter parameters to the report. Show a loading skeleton while the report loads. Use a professional dark sidebar with white content area and subtle borders between sections.
Copy this prompt to try it in V0
Client-Facing Analytics Portal
A portal where each client can view their own Power BI reports, filtered to show only their data. The Next.js app controls which report and which row-level security role applies to each client, so clients only see their own data even though multiple clients share the same Power BI report.
Build a client analytics portal with a welcome header showing 'Your Analytics Dashboard, [Client Name]'. Display three metric summary cards (Total Revenue, Active Users, Conversion Rate) with trend arrows pulled from /api/analytics/summary. Below the cards, show the main Power BI report embedded full-width, loading the embed token from /api/powerbi/embed-token?reportId=REPORT_ID&clientId=CLIENT_ID. Add a report page selector dropdown above the embedded report for switching between report pages. Use the client's brand colors from a theme prop and a clean modern layout.
Copy this prompt to try it in V0
Operations Monitoring with Real-Time Filters
A real-time operations dashboard that embeds a Power BI report and allows operators to apply department, region, and time period filters from the Next.js UI. The filter selections send filter queries to the embedded report using Power BI's JavaScript API, updating the report view without reloading the page.
Create an operations monitoring dashboard with a top filter bar containing three dropdowns: Department (Sales/Marketing/Engineering/HR), Region (North/South/East/West), and Time Period (Today/This Week/This Month/This Quarter). The main area embeds a Power BI report that updates when filters change, calling applyFilters() on the embedded report using the powerbi-client API. Show filter-applied badges below the filter bar. Add a 'Reset Filters' button. Below the report, show a data freshness indicator with last-updated timestamp. Use a clean light theme with blue filter controls.
Copy this prompt to try it in V0
Troubleshooting
Azure AD token request fails with 'AADSTS700016: Application was not found in the directory'
Cause: The AZURE_CLIENT_ID does not match an app registration in the tenant specified by AZURE_TENANT_ID, or the app registration is in a different Azure AD tenant.
Solution: In the Azure Portal → Azure Active Directory → App registrations, verify the app exists and note both the Application (client) ID and the Directory (tenant) ID from the Overview page. Both should match exactly what is stored in Vercel. The tenant ID is the ID of your Azure AD directory, not of a subscription.
Power BI GenerateToken returns 403 Forbidden
Cause: The Azure AD service principal (your app registration) has not been added to the Power BI workspace as a member, or the Power BI workspace does not have the service principal feature enabled.
Solution: In Power BI at app.powerbi.com, navigate to the workspace → Settings → Access tab → enter your Azure AD app's display name or client ID → add as Member or Admin. Also verify in Power BI Admin Portal (admin.powerbi.com) → Tenant settings → Developer settings → 'Allow service principals to use Power BI APIs' is enabled for your security group or 'The entire organization'.
powerbi-client causes 'window is not defined' error during build
Cause: The powerbi-client library accesses browser-only APIs (window, document) and was imported at the module level in a component that runs during Next.js server-side rendering.
Solution: Use dynamic import inside a useEffect hook or use Next.js's dynamic() function with { ssr: false } to load the powerbi-client library only in the browser. Never import powerbi-client at the top level of a component file — it must be loaded dynamically after the component mounts.
1// Use dynamic import inside useEffect:2useEffect(() => {3 import('powerbi-client').then(({ service, factories }) => {4 // Initialize Power BI here5 });6}, [embedConfig]);Embed token expires after 60 minutes and the report stops working
Cause: Power BI embed tokens are valid for 60 minutes by default and the client component does not implement token refresh, causing the report to become unresponsive after the token expires.
Solution: Implement token refresh using a useEffect timer that fetches a new embed token from your API route 5 minutes before expiry. Use the expiry timestamp returned by your API route to calculate the refresh timing. Call report.setAccessToken(newToken) on the embedded report instance to update the token without reloading the full report.
Best practices
- Never expose AZURE_CLIENT_SECRET or embed tokens to the browser — always generate embed tokens server-side in API routes and return only the short-lived token to the client
- Cache Azure AD access tokens in a module-level variable with expiry checking to avoid redundant authentication roundtrips — one Azure AD token lasts 60 minutes and can serve many embed token requests
- Implement embed token refresh in your client component rather than forcing a full page reload — Power BI's JavaScript API supports setAccessToken() for seamless token renewal
- Use Power BI row-level security (RLS) when embedding reports for multiple tenants or clients — always specify identities in the GenerateToken request rather than relying on data-level restrictions in the report design
- Set panes.filters.visible to false in the embed configuration for end-user-facing apps — apply filters programmatically from your Next.js UI instead of showing Power BI's native filter pane
- Store workspace and report IDs as environment variables so you can update report content in Power BI Desktop and publish without changing your application code
- Monitor Azure AD app client secret expiry dates — set up alerts 30 days before expiry to avoid service interruptions from expired credentials
Alternatives
Use Looker if your organization is on Google Cloud or Salesforce — Looker's embedding API follows a similar signed URL pattern but integrates natively with BigQuery and other Google data sources.
Choose Google Data Studio (Looker Studio) if you're already in the Google ecosystem and want a free alternative to Power BI Embedded — Looker Studio reports can be embedded via iframe without API authentication.
Consider Google Analytics if you primarily need web traffic and user behavior data rather than business intelligence reports — Google Analytics has a simpler API that doesn't require Azure AD setup.
Frequently asked questions
Do I need a paid Power BI license to embed reports?
Yes — embedding Power BI reports in your own application using the service principal approach requires either Power BI Premium Per User (PPU) or Power BI Embedded A-SKU capacity. Individual users viewing embedded reports do not need Power BI licenses when using A-SKU capacity, which is the model described in this guide. For testing and development, you can use a Power BI Pro trial account and embed in 'development mode' without purchasing capacity.
What's the difference between a Power BI embed token and an Azure AD access token?
The Azure AD access token authenticates your application to the Power BI REST API — it's a server-side credential that proves your Azure AD app has permission to call Power BI's APIs. The embed token is generated by calling Power BI's GenerateToken endpoint using the Azure AD token, and it's a short-lived credential that the browser uses to load and interact with a specific report. The Azure AD token is highly sensitive and must stay server-side; the embed token can be sent to the browser because it only grants access to one specific report.
How do I apply filters from my Next.js UI to the embedded Power BI report?
After the Power BI report loads, use the report object returned by powerbi.embed() to call report.setFilters(filtersArray). Power BI filters are defined using the models.BasicFilter or models.AdvancedFilter classes from powerbi-models. Listen for the report's 'loaded' event before attempting to apply filters — applying filters before the report loads will fail silently. Store the report object in a ref so you can call setFilters() from outside the useEffect where it was created.
Can I embed Power BI dashboards in addition to reports?
Yes — change the type in the embed configuration from 'report' to 'dashboard' and use the dashboard's embed URL and a dashboard-specific embed token from the GenerateToken endpoint at /groups/{workspace_id}/dashboards/{dashboard_id}/GenerateToken. The powerbi-client library supports both report and dashboard embedding with the same API surface, but some features like filter application are report-specific and not available for dashboards.
How do I get the Power BI workspace ID and report ID?
Navigate to your report in Power BI at app.powerbi.com. The URL follows this pattern: app.powerbi.com/groups/{workspace_id}/reports/{report_id}/ReportSection. Copy the workspace ID (the UUID after /groups/) and the report ID (the UUID after /reports/). Both are standard UUID format. You can also use the Power BI REST API endpoint GET /groups to list all workspaces and their IDs programmatically.
Will embedding Power BI work on Vercel's Hobby plan?
Yes — the Next.js API route that fetches embed tokens is a short-running serverless function that completes in well under 10 seconds. The actual report rendering happens entirely in the browser using the powerbi-client library and the embed token — no server resources are needed for the report display itself. The only Vercel resource used is the brief API route execution for token generation.
How do I handle multiple reports in the same dashboard?
Store multiple report IDs in your database or as environment variables (POWERBI_REPORT_ID_SALES, POWERBI_REPORT_ID_OPERATIONS). Your API route accepts a reportId query parameter and fetches the embed token for the specified report. In your Next.js component, pass the selected report ID to the PowerBIViewer component as a prop, and the component re-fetches the embed token whenever reportId changes. For production apps, RapidDev's team can help design a multi-report dashboard architecture with proper role-based access control.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation