# How to Integrate Microsoft Power BI with V0

- Tool: V0
- Difficulty: Intermediate
- Time required: 45 minutes
- Last updated: April 2026

## TL;DR

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.

## Before you start

- 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

### 1. 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.

**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.

### 2. 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.

```
// app/api/powerbi/embed-token/route.ts
import { NextRequest, NextResponse } from 'next/server';

async function getAzureAccessToken(): Promise<string> {
  const tenantId = process.env.AZURE_TENANT_ID;
  const clientId = process.env.AZURE_CLIENT_ID;
  const clientSecret = process.env.AZURE_CLIENT_SECRET;

  if (!tenantId || !clientId || !clientSecret) {
    throw new Error('Azure AD credentials are not configured');
  }

  const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;

  const response = await fetch(tokenUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'client_credentials',
      client_id: clientId,
      client_secret: clientSecret,
      scope: 'https://analysis.windows.net/powerbi/api/.default',
    }),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.error_description || 'Azure AD authentication failed');
  }

  const data = await response.json();
  return data.access_token;
}

export async function GET(request: NextRequest) {
  const workspaceId = process.env.POWERBI_WORKSPACE_ID;
  const reportId = request.nextUrl.searchParams.get('reportId') || process.env.POWERBI_REPORT_ID;

  if (!workspaceId || !reportId) {
    return NextResponse.json(
      { error: 'Power BI workspace or report ID is not configured' },
      { status: 500 }
    );
  }

  try {
    const accessToken = await getAzureAccessToken();

    // Get report details (embed URL)
    const reportUrl = `https://api.powerbi.com/v1.0/myorg/groups/${workspaceId}/reports/${reportId}`;
    const reportResponse = await fetch(reportUrl, {
      headers: { Authorization: `Bearer ${accessToken}` },
    });

    if (!reportResponse.ok) {
      throw new Error(`Failed to fetch report: ${reportResponse.statusText}`);
    }

    const report = await reportResponse.json();

    // Generate embed token
    const embedTokenUrl = `https://api.powerbi.com/v1.0/myorg/groups/${workspaceId}/reports/${reportId}/GenerateToken`;
    const tokenResponse = await fetch(embedTokenUrl, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${accessToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ accessLevel: 'View' }),
    });

    if (!tokenResponse.ok) {
      throw new Error(`Failed to generate embed token: ${tokenResponse.statusText}`);
    }

    const tokenData = await tokenResponse.json();

    return NextResponse.json({
      embedToken: tokenData.token,
      embedUrl: report.embedUrl,
      reportId,
      expiry: tokenData.expiration,
    });
  } catch (error) {
    const message = error instanceof Error ? error.message : 'Failed to generate embed token';
    console.error('Power BI embed error:', message);
    return NextResponse.json({ error: message }, { status: 500 });
  }
}
```

**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.

### 3. 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.

```
// components/PowerBIViewer.tsx
'use client';

import { useEffect, useState, useRef } from 'react';

interface EmbedConfig {
  embedToken: string;
  embedUrl: string;
  reportId: string;
  expiry: string;
}

interface PowerBIViewerProps {
  reportId?: string;
  height?: number;
}

export function PowerBIViewer({ reportId, height = 700 }: PowerBIViewerProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const [config, setConfig] = useState<EmbedConfig | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchEmbedConfig = async () => {
      setLoading(true);
      setError(null);
      try {
        const params = reportId ? `?reportId=${reportId}` : '';
        const response = await fetch(`/api/powerbi/embed-token${params}`);
        const data = await response.json();
        if (!response.ok) throw new Error(data.error || 'Failed to load report');
        setConfig(data);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Unknown error');
      } finally {
        setLoading(false);
      }
    };

    fetchEmbedConfig();
  }, [reportId]);

  useEffect(() => {
    if (!config || !containerRef.current || typeof window === 'undefined') return;

    // Dynamically import powerbi-client to avoid SSR issues
    import('powerbi-client').then(({ service, factories, models }) => {
      const powerbi = new service.Service(
        factories.hpmFactory,
        factories.wpmpFactory,
        factories.routerFactory
      );

      const embedConfig = {
        type: 'report',
        id: config.reportId,
        embedUrl: config.embedUrl,
        accessToken: config.embedToken,
        tokenType: models.TokenType.Embed,
        settings: {
          panes: { filters: { visible: false }, pageNavigation: { visible: true } },
          background: models.BackgroundType.Transparent,
        },
      };

      powerbi.embed(containerRef.current!, embedConfig);
    });
  }, [config]);

  if (loading) {
    return (
      <div className="animate-pulse bg-gray-200 rounded-lg" style={{ height }} />
    );
  }

  if (error) {
    return (
      <div className="flex items-center justify-center bg-red-50 border border-red-200 rounded-lg" style={{ height }}>
        <p className="text-red-600 text-sm">{error}</p>
      </div>
    );
  }

  return <div ref={containerRef} style={{ height, width: '100%' }} />;
}
```

**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.

### 4. 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.

**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.

## 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

## 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.

Prompt example:

```
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.
```

### 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.

Prompt example:

```
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.
```

### 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.

Prompt example:

```
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.
```

## 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.

```
// Use dynamic import inside useEffect:
useEffect(() => {
  import('powerbi-client').then(({ service, factories }) => {
    // Initialize Power BI here
  });
}, [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.

## 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.

---

Source: https://www.rapidevelopers.com/v0-integrations/microsoft-power-bi
© RapidDev — https://www.rapidevelopers.com/v0-integrations/microsoft-power-bi
