To use Looker with V0 by Vercel, embed Looker dashboards and Explores in your Next.js app using the Looker Embed SDK, or fetch data programmatically via the Looker API through a server-side API route. Store your Looker API client credentials as server-only Vercel environment variables and generate signed SSO embed URLs in an API route to securely embed live Looker content.
Embedding Looker Analytics in V0-Generated Applications
Looker (now Google Cloud Looker) is a semantic layer BI platform — rather than storing precomputed reports, Looker generates SQL from LookML model definitions at query time, allowing users to explore data consistently regardless of which dashboard or interface they use. For V0 developers, the most powerful integration pattern is Looker Embedded Analytics: placing Looker dashboards, Explores, and Looks directly inside your application so users get live analytics without switching to a separate BI tool.
Looker provides two embedding approaches. Signed SSO embedding is the production-ready pattern — your Next.js API route generates a time-limited, cryptographically signed URL that embeds a Looker dashboard with specific user context and permissions. When a user visits your app, the API route creates an embed URL tied to that user's identity, and the Looker Embed SDK renders the dashboard in a managed iframe. The user authenticates via the signed URL, not by logging into Looker directly.
The second approach is the Looker API — a REST API that lets you run saved Looks, execute inline queries, fetch metadata, and manage content programmatically. This is useful for pulling specific data values into your V0 components rather than embedding full dashboards. V0 can generate the surrounding UI (filters, navigation, summary cards) while Looker handles the data layer, creating a seamless analytics experience that lives entirely within your application.
Integration method
V0 generates the React UI wrapper and surrounding page layout. A Next.js API route handles Looker API authentication using client credentials stored as server-only environment variables, generates signed SSO embed URLs for secure dashboard embedding, and fetches query results from Looker's Run Query API. The Looker Embed SDK runs in the browser to render embedded dashboards inside iframes with controlled communication.
Prerequisites
- A V0 account at v0.dev with a Next.js project
- A Looker instance (Google Cloud Looker or legacy Looker) with API access enabled
- A Looker API client ID and client secret from your Looker admin (Admin → Users → your user → API Keys)
- Embedded analytics enabled on your Looker instance (Admin → Platform → Embedding) with your domain whitelisted
- Your Looker instance host URL (e.g., https://yourcompany.cloud.looker.com)
Step-by-step guide
Generate the Dashboard Wrapper UI in V0
Generate the Dashboard Wrapper UI in V0
Open V0 and design the page that will host your embedded Looker dashboard. This involves two parts: the surrounding application chrome (navigation, filter controls, header) and a placeholder container for the Looker iframe. At this stage, V0 doesn't know about Looker — you're building the frame that will surround the embedded dashboard. Describe the layout clearly to V0. A typical analytics portal page has a fixed navigation header with your app's branding, a filter bar below the header with date pickers and dimension selectors, a main content area that will hold the Looker iframe, and optionally a sidebar or bottom section with supplementary metrics pulled from the Looker API. Ask V0 to create the placeholder container as a div with a specific height (Looker dashboards need an explicit height to render properly — a good default is at least 600px or a full viewport height calculation). Also ask for a loading state that shows while the Looker Embed SDK initializes, since there's a brief delay before the dashboard becomes interactive. The filter controls V0 generates will later pass values to the embedded Looker dashboard via the Embed SDK's postMessage interface. Design the filters to match your Looker dashboard's filter fields — the filter field names need to match exactly what Looker expects.
Create an analytics dashboard page with a sticky header showing a Looker logo placeholder and navigation links. Below the header, add a filter bar with a date range picker, a dropdown for 'Region' (options: All Regions, North America, Europe, Asia Pacific), and a 'Refresh' button. The main content area should have a tall container div (min-height: 700px) with a loading spinner while the dashboard loads. Add a footer showing last updated time. Style with a clean white/light gray enterprise look.
Paste this in V0 chat
Pro tip: Set the Looker embed container to a minimum height of 600-700px — Looker calculates its responsive layout based on the iframe dimensions, and containers that are too short produce cramped or cut-off dashboards.
Expected result: A polished analytics page layout renders in V0 with a branded header, filter controls, a tall dashboard container with loading state, and a clean enterprise visual design.
Create the SSO Embed URL API Route
Create the SSO Embed URL API Route
Looker's Signed SSO embedding requires your server to generate a cryptographically signed URL for each embed session. The URL encodes the user's identity, their Looker permissions, any dashboard filters, and a timestamp — Looker verifies the signature before serving the embedded content. This signing process must happen server-side because it requires your Looker embed secret key. Create an API route at app/api/looker/embed-url/route.ts. The route uses the @looker/embed-sdk package or manual HMAC-SHA1 signing (the Looker embed URL format uses SHA1, not SHA256). The signed URL construction involves: the embed path (e.g., /embed/dashboards/42), the host domain of your application, the user's external ID and name, permissions array, model access, user attributes for row-level security, a session length in seconds, and a current timestamp. All these parameters are assembled into a specific query string, then HMAC-SHA1 signed with your embed secret. Install the @looker/embed-sdk package which includes URL generation utilities. Alternatively, build the URL manually using Node's crypto module with createHmac('sha1', embedSecret). The embed SDK approach is less error-prone because the parameter ordering is critical for the signature to validate — any deviation causes Looker to reject the embed with a 'Signature check failed' error. Return the signed URL from the API route. The frontend will use this URL as the src for the Looker Embed SDK's iframe initialization. Include an expiration check — signed URLs are only valid for the session_length you specify (typically 600 seconds for a 10-minute session), after which the dashboard will prompt for re-authentication.
Create a Next.js API route at app/api/looker/embed-url/route.ts. It should read LOOKER_EMBED_SECRET, LOOKER_HOST, and LOOKER_EMBED_SECRET from process.env. Accept dashboard_id and filters as query parameters. Generate a Looker SSO embed URL by building the embed path, setting user permissions to ['access_data','see_looks','see_user_dashboards','explore'], and HMAC-SHA1 signing the URL with the embed secret using Node's crypto module. Return the signed URL as JSON.
Paste this in V0 chat
1import { NextRequest, NextResponse } from 'next/server';2import crypto from 'crypto';34function signLookerUrl(params: {5 host: string;6 embedSecret: string;7 dashboardId: string;8 externalUserId: string;9 userName: string;10 permissions: string[];11 models: string[];12 sessionLength: number;13 filters?: Record<string, string>;14}): string {15 const { host, embedSecret, dashboardId, externalUserId, userName, permissions, models, sessionLength, filters } = params;1617 const nonce = crypto.randomBytes(16).toString('hex');18 const time = Math.floor(Date.now() / 1000);19 const embedPath = `/embed/dashboards/${dashboardId}`;2021 const filterParams = filters22 ? '?' + new URLSearchParams(filters).toString()23 : '';2425 const stringToSign = [26 host,27 embedPath + filterParams,28 nonce,29 time.toString(),30 sessionLength.toString(),31 externalUserId,32 userName,33 JSON.stringify(permissions),34 JSON.stringify(models),35 '{}', // group_ids36 '{}', // external_group_id37 '{}', // user_attributes38 '', // access_filters39 ].join('\n');4041 const signature = crypto42 .createHmac('sha1', embedSecret)43 .update(stringToSign)44 .digest('base64');4546 const url = new URL(`https://${host}${embedPath}${filterParams}`);47 url.searchParams.set('nonce', nonce);48 url.searchParams.set('time', time.toString());49 url.searchParams.set('session_length', sessionLength.toString());50 url.searchParams.set('external_user_id', externalUserId);51 url.searchParams.set('first_name', userName.split(' ')[0]);52 url.searchParams.set('last_name', userName.split(' ').slice(1).join(' '));53 url.searchParams.set('permissions', JSON.stringify(permissions));54 url.searchParams.set('models', JSON.stringify(models));55 url.searchParams.set('signature', signature);5657 return url.toString();58}5960export async function GET(req: NextRequest) {61 const { searchParams } = new URL(req.url);62 const dashboardId = searchParams.get('dashboard_id') ?? '1';6364 const embedSecret = process.env.LOOKER_EMBED_SECRET;65 const host = process.env.LOOKER_HOST;6667 if (!embedSecret || !host) {68 return NextResponse.json({ error: 'Looker credentials not configured' }, { status: 500 });69 }7071 const embedUrl = signLookerUrl({72 host,73 embedSecret,74 dashboardId,75 externalUserId: 'user-001',76 userName: 'Dashboard User',77 permissions: ['access_data', 'see_looks', 'see_user_dashboards', 'explore'],78 models: ['all'],79 sessionLength: 600,80 });8182 return NextResponse.json({ embedUrl });83}Pro tip: The order of parameters in the string-to-sign is exact and documented by Looker — any reordering causes signature validation to fail silently. Test your URL generator against Looker's embed URL validator in the Admin panel before deploying.
Expected result: GET /api/looker/embed-url?dashboard_id=42 returns a signed Looker SSO embed URL. When opened directly in a browser, it loads the Looker dashboard without prompting for login credentials.
Initialize the Looker Embed SDK in Your React Component
Initialize the Looker Embed SDK in Your React Component
Now connect the V0-generated dashboard container to Looker using the Embed SDK. Install @looker/embed-sdk and initialize it in a 'use client' component. The Embed SDK manages the iframe lifecycle, cross-origin messaging between your app and the Looker iframe, and filter synchronization. In your component, fetch the signed embed URL from your API route and then initialize the Embed SDK targeting the container div. The SDK's LookerEmbedSDK.init() method takes your Looker host, and then you chain builder methods to specify the dashboard ID, container element, filter values, and event handlers. The most powerful feature of the Embed SDK is bidirectional communication. You can send filter updates to the Looker dashboard without reloading the iframe using dashboard.send('dashboard:filters:update', { filters }). You can also listen for events from the Looker dashboard — when a user clicks a data point, Looker fires a drilldown event that your app can intercept to show additional detail in your own components. Handle the iframe initialization in a useEffect that runs once on mount. The Embed SDK is not SSR-compatible (it uses window and document), so the component must be a client component. Pass the container ref to the SDK's appendTo() method. Clean up by storing the dashboard connection and calling dashboard.disconnect() when the component unmounts to prevent memory leaks. For the filter controls you designed in Step 1, wire the filter state to dashboard.send('dashboard:filters:update') calls — each time the user changes a filter, the embedded Looker dashboard updates without a full reload.
Create a client component LookerDashboard.tsx that fetches a signed embed URL from /api/looker/embed-url with a dashboard_id prop. Initialize @looker/embed-sdk with LookerEmbedSDK.init() using NEXT_PUBLIC_LOOKER_HOST. Create the dashboard connection with createDashboardWithId(), appendTo a container ref, add filters from props, and call build() then connect(). Show a loading state until connected. Export a setFilters method via useImperativeHandle so parent components can push filter updates.
Paste this in V0 chat
1'use client';23import { useEffect, useRef, useState } from 'react';4import { LookerEmbedSDK } from '@looker/embed-sdk';56interface LookerDashboardProps {7 dashboardId: string;8 filters?: Record<string, string>;9}1011export default function LookerDashboard({ dashboardId, filters = {} }: LookerDashboardProps) {12 const containerRef = useRef<HTMLDivElement>(null);13 const [loading, setLoading] = useState(true);14 const [error, setError] = useState<string | null>(null);1516 useEffect(() => {17 const lookerHost = process.env.NEXT_PUBLIC_LOOKER_HOST;18 if (!lookerHost || !containerRef.current) return;1920 LookerEmbedSDK.init(lookerHost, '/api/looker/auth');2122 LookerEmbedSDK.createDashboardWithId(dashboardId)23 .appendTo(containerRef.current)24 .withFilters(filters)25 .on('dashboard:run:complete', () => setLoading(false))26 .build()27 .connect()28 .then(() => setLoading(false))29 .catch((err) => {30 setError('Failed to connect to Looker');31 console.error(err);32 });33 }, [dashboardId]);3435 return (36 <div style={{ position: 'relative', height: '700px' }}>37 {loading && (38 <div className="absolute inset-0 flex items-center justify-center bg-white">39 <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />40 </div>41 )}42 {error && <div className="p-4 text-red-600">{error}</div>}43 <div ref={containerRef} style={{ height: '100%', width: '100%' }} />44 </div>45 );46}Pro tip: The Looker Embed SDK requires your application's domain to be whitelisted in the Looker Admin panel under Admin → Platform → Embedding → Embedded Domain Allowlist. Add both your production domain and localhost:3000 for local development.
Expected result: The LookerDashboard component renders a Looker dashboard inside your V0-generated page layout, with the loading spinner disappearing once the dashboard finishes its first data load. The Looker chrome (header, filters) is visible inside the iframe.
Add Environment Variables in Vercel and Deploy
Add Environment Variables in Vercel and Deploy
Looker embedding requires three environment variables. Two are server-only secrets used by your API route to generate signed embed URLs. One is a public variable needed by the Embed SDK running in the browser. The server-only variables are LOOKER_EMBED_SECRET (the embed secret from Looker Admin → Platform → Embedding → Embed Secret) and LOOKER_API_CLIENT_SECRET (the API key secret from Admin → Users → API Keys). Never add the NEXT_PUBLIC_ prefix to these — they're cryptographic secrets that must not appear in the browser. The public variable is NEXT_PUBLIC_LOOKER_HOST — your Looker instance hostname (e.g., yourcompany.cloud.looker.com without the https:// prefix). The Embed SDK needs this value client-side to initialize the cross-origin messaging channel. In the Vercel Dashboard, go to your project → Settings → Environment Variables. Add all three variables for both Production and Preview environments. For Preview environments, you can use the same Looker instance but consider restricting the Preview user's permissions in the embed URL generation to read-only access. After adding variables, trigger a redeployment. Visit the deployed URL and verify the Looker dashboard loads. If you see a blank iframe or 'Access Denied' from Looker, check that your deployed domain is in the Looker embed allowlist (Admin → Platform → Embedding → Embedded Domain Allowlist) and that the LOOKER_EMBED_SECRET matches exactly what Looker shows in the admin panel. For complex permission setups across multiple customer accounts, RapidDev can help structure the user attribute system for row-level security.
1# .env.local — never commit this file2# Server-only Looker credentials (no NEXT_PUBLIC_ prefix)3LOOKER_EMBED_SECRET=abc123...4LOOKER_API_CLIENT_ID=xyz7895LOOKER_API_CLIENT_SECRET=def456...6LOOKER_HOST=yourcompany.cloud.looker.com78# Public — needed by Embed SDK in browser9NEXT_PUBLIC_LOOKER_HOST=yourcompany.cloud.looker.comPro tip: Add your Vercel preview URL pattern (*.vercel.app) to the Looker embed allowlist alongside your production domain, otherwise preview deployments will show blank embed containers.
Expected result: All Looker environment variables are set in Vercel. The deployed app embeds live Looker dashboards with correct authentication. Server credentials are never exposed in browser network requests.
Common use cases
Embedded Customer Analytics Portal
Build a customer-facing analytics portal where each customer sees only their own data. The API route generates an SSO embed URL with the customer's ID as a filter parameter and user attribute, ensuring complete data isolation via Looker's user attribute system. Customers get the full Looker explore experience without a Looker license.
Create an analytics portal page with a Looker dashboard embedded in the main content area. Add a sidebar with filter controls for date range and product category. When filters change, update the embedded dashboard via postMessage. Include a fullscreen button and a 'Download CSV' button that calls the Looker API to export current query results.
Copy this prompt to try it in V0
Executive KPI Dashboard with Live Looker Data
Fetch specific metric values from Looker's Run Query API and display them in a custom-designed V0 dashboard. Instead of embedding a full Looker dashboard, pull just the numbers you need (revenue, churn rate, NPS) and render them in V0-generated KPI cards with trend indicators.
Build an executive dashboard with 6 KPI cards in a 3x2 grid. Each card shows a metric name, current value formatted as currency or percentage, a trend arrow (up/down), and a sparkline chart showing the last 30 days. Data comes from an API call to /api/looker/query. Add a loading skeleton for each card and a last-refreshed timestamp.
Copy this prompt to try it in V0
Data Exploration Interface
Embed a Looker Explore (the query-building interface) directly in your app for power users who need to build their own queries. Wrap the Explore with a branded navigation header and a saved-queries sidebar that calls the Looker API to list and load previously saved Looks.
Create a data exploration page with a Looker Explore embedded in a full-height iframe. Add a header bar with your app's branding and a 'Saved Reports' drawer on the right side showing a list of saved Looks from the API. Each list item should show the Look name, creator, and last run date. Clicking a saved report should load it in the embedded Explore.
Copy this prompt to try it in V0
Troubleshooting
Looker dashboard shows a blank white iframe or 'Access Denied' message after loading
Cause: Your application's domain is not in Looker's Embedded Domain Allowlist, or the signed embed URL's signature is invalid due to a parameter mismatch.
Solution: In your Looker admin panel go to Admin → Platform → Embedding and add your exact domain (including protocol: https://yourapp.vercel.app) to the allowlist. If the domain is already there, validate the embed URL signature using the Looker Embed URL Helper tool in the admin panel. Ensure LOOKER_EMBED_SECRET matches exactly what the admin panel shows.
'Signature check failed' error appears in the Looker embed
Cause: The HMAC-SHA1 signature on the embed URL doesn't match what Looker computed. This is almost always a parameter ordering issue in the string-to-sign construction.
Solution: Verify the parameter order in your string-to-sign exactly matches Looker's documented format: host, embed_path, nonce, time, session_length, external_user_id, first_name, last_name, permissions, models, group_ids, external_group_id, user_attributes, access_filters. Any deviation — including extra spaces, different JSON serialization, or missing empty strings for unused fields — causes signature failure.
1// Double-check your stringToSign joins with newlines, not other separators2const stringToSign = [3 host, // 'yourcompany.cloud.looker.com'4 embedPath, // '/embed/dashboards/42'5 nonce, // random hex string6 time.toString(),7 sessionLength.toString(),8 externalUserId,9 firstName,10 lastName,11 JSON.stringify(permissions), // must be compact JSON12 JSON.stringify(models),13 '{}', // group_ids (empty object string if unused)14 '', // external_group_id (empty string if unused)15 '{}', // user_attributes16 '', // access_filters17].join('\n');Looker Embed SDK initialization fails with 'Cannot read properties of null (reading appendChild)'
Cause: The Embed SDK's appendTo() is called before the ref element is mounted, or the component is running server-side where document is undefined.
Solution: Ensure the component has the 'use client' directive and the SDK initialization is inside a useEffect hook that only runs when containerRef.current is not null. Check that the container div is rendered before useEffect fires.
1useEffect(() => {2 // Guard against SSR and unmounted refs3 if (typeof window === 'undefined' || !containerRef.current) return;4 // Initialize SDK here5}, []);Looker API calls return 401 Unauthorized even with correct client credentials
Cause: Looker API authentication uses a two-step OAuth flow — you first exchange your client ID and secret for a short-lived access token, then use that token for API calls. Many developers try to use the client secret directly as a Bearer token, which does not work.
Solution: Call the Looker API login endpoint first: POST https://{your-looker-host}/api/4.0/login with client_id and client_secret as form-encoded body parameters. This returns an access_token (typically valid for 1 hour). Use that token as the Authorization: Bearer {token} header for all subsequent API calls.
1// Get Looker API access token2async function getLookerToken(): Promise<string> {3 const res = await fetch(`https://${process.env.LOOKER_HOST}/api/4.0/login`, {4 method: 'POST',5 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },6 body: new URLSearchParams({7 client_id: process.env.LOOKER_API_CLIENT_ID!,8 client_secret: process.env.LOOKER_API_CLIENT_SECRET!,9 }),10 });11 const { access_token } = await res.json();12 return access_token;13}Best practices
- Whitelist both your production domain and your Vercel preview URL pattern in Looker's Embedded Domain Allowlist before deploying — missing the preview domain causes confusing blank embeds during testing
- Cache Looker API access tokens server-side for up to 55 minutes (they expire at 60 minutes) to avoid authenticating on every API request — use Upstash Redis or Vercel's Data Cache for token storage
- Use Looker's user attribute system for row-level security in multi-tenant apps rather than trying to filter at the API route level — user attributes embedded in the SSO URL are enforced by Looker's database queries
- Set a realistic session_length on embed URLs (600-3600 seconds) and handle session expiration by fetching a fresh embed URL — expired sessions show a Looker login prompt inside your iframe
- Prefer the Embed SDK's filter update mechanism over reloading the iframe when users change filters — dashboard re-runs cost quota, and iframe reloads cause jarring visual resets
- Never expose LOOKER_EMBED_SECRET as a NEXT_PUBLIC_ variable — anyone with the embed secret can generate valid SSO URLs impersonating any user on your instance
- Test your embed URL generation against Looker's built-in embed URL validator in Admin → Platform → Embedding before debugging production issues
Alternatives
Power BI is better for organizations already in the Microsoft 365 ecosystem — it integrates natively with Azure and Excel data sources, while Looker is stronger for Google Cloud and data warehouse-centric architectures.
Tableau is a better choice for teams that prioritize visual analytics and self-service report building — Tableau's drag-and-drop interface is more accessible for non-technical users than Looker's LookML semantic layer.
Frequently asked questions
Do I need a Looker license to embed dashboards in my V0 app?
Yes, you need a Looker instance with the Embedded Analytics feature enabled. Looker pricing is enterprise-tier and based on users and data volume. SSO embedding typically requires the Looker Platform license. However, your end users (the people viewing the embedded dashboards) don't need individual Looker seats — that's the main value of embedded analytics.
What's the difference between Looker SSO embedding and iFrame embedding?
SSO embedding is the production-ready method: your server generates a signed URL with user context, permissions, and filters, and Looker authenticates the embed session without the user seeing a login form. iFrame embedding is simpler but requires the user to already be logged into Looker, making it unsuitable for sharing with external users or customers who don't have Looker accounts.
Can V0 generate the Looker embed integration code automatically?
V0 can generate the surrounding UI, API route structure, and React component shell, but it won't know your specific Looker dashboard IDs, LookML model names, or SSO embed URL signing algorithm without context. Use the prompts in this tutorial to guide V0, then fill in your specific dashboard IDs and Looker host in the generated code.
How do I pass user-specific data isolation to Looker embeds?
Use Looker user attributes. In the Looker admin panel, define a user attribute like 'company_id'. When generating the SSO embed URL, include user_attributes: { company_id: currentUser.companyId } in the signed parameters. Your LookML developer then adds a filter on the model that restricts queries using the company_id user attribute, ensuring each embed session only returns data for that user's company.
How is Looker different from Power BI for V0 integrations?
Both Looker and Power BI support embedded analytics in Next.js apps, but the technical approach differs. Looker uses HMAC-signed SSO URLs for authentication. Power BI uses Azure Active Directory OAuth tokens. Looker's Embed SDK provides richer cross-frame communication for filter synchronization. Power BI's embedding relies on the Power BI JavaScript SDK (powerbi-client) for similar functionality.
Can I use the Looker API to run queries without embedding a full dashboard?
Yes. Looker's Run Inline Query API endpoint (POST /api/4.0/queries/run/json) lets you execute arbitrary LookML queries and get results as JSON. This is useful for pulling specific KPIs into V0 components without showing the full Looker dashboard UI. Authenticate with an API access token (from POST /api/4.0/login) and pass your model name, view name, dimensions, measures, and filters in the request body.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation