Embedded Analytics in React: A Developer’s Guide (2026)
How to embed analytics and dashboards in a React app - SDKs, authentication, row-level security, Next.js App Router, theming, and how Databrain, Metabase, Cube, Embeddable, and Lightdash compare.
Key Takeaways
- Embedding analytics into a React app is a 1–5 day project; building the same capability from scratch is 2–6 months once you account for chart types, drill-downs, multi-tenancy, and exports.
- There are three architectural patterns for embedding: iframes (zero integration, poor UX), JavaScript SDKs (tight React integration, bundle-size tax), and web components (Shadow DOM isolation, framework-agnostic). For customer-facing SaaS, SDK or web components win.
- Never put your analytics vendor's API key on the frontend. Every production embed uses a short-lived guest token minted by your backend — and if you scope the token to a tenant ID, you get row-level security with minimal extra work (assuming your metric definitions filter on that field).
- Next.js App Router works fine: token generation as a Route Handler, embed as a Client Component (
'use client'), consumed from any Server Component page. - On the embedded React SERP, Databrain, Metabase SDK, Cube, Embeddable, Lightdash, and Reveal all compete on slightly different axes — pick based on whether you need a full dashboard UI, a headless semantic layer, or dbt integration.
When to Embed vs. Build From Scratch
Before the code, the honest decision:
Build custom when:
- Your dashboard is the product (Mixpanel, Amplitude, Observable)
- Your UI has to do something no vendor supports (highly interactive simulations, custom workflows)
- You have a dedicated analytics team and a multi-year horizon
Embed when:
- Analytics is a feature inside a broader product, not the product itself
- You have multi-tenant data (each customer sees only their rows)
- You need more than 5–6 chart types
- You need exports, scheduled reports, drill-downs, and filter bars out of the box
- You want to ship this quarter, not next year
Most SaaS teams we've worked with started by building custom (or forking Apache Superset / Metabase OSS), then switched to embedding when the maintenance cost of 20+ chart types with drill-downs and cross-filtering became the breaking point. BerryBox, Freightify, and SpotDraft all went through this pattern — see their stories.
If you're still deciding, our embedded analytics build vs. buy guide goes into the full engineering-cost math (multi-tenancy and AI infrastructure are where most build estimates blow up). For the broader context on what embedded analytics means, see the complete embedded analytics guide. This article assumes you've decided to embed in a React app.
The Three Architectural Patterns
There are three ways analytics platforms integrate into a React app. It's worth understanding the difference because it affects bundle size, styling, auth, and how tightly you can customize.
1. iframes
The analytics platform hosts your dashboard; you embed it via <iframe src="...">.
- Pros: zero bundle-size impact on your app, full style isolation, trivial to implement
- Cons: poor interactivity with the parent page, awkward auth (cross-origin cookies), can't style beyond what the vendor exposes, resizing is a pain
- Who uses this: Grafana, older Power BI embeds, Tableau (classic mode), Looker
iframe embedding is fine for internal tools. For customer-facing SaaS, you usually want something better.
2. JavaScript SDKs
The vendor ships an npm package you install directly. You get React components (<MetabaseProvider>, <InteractiveQuestion>, etc.) that render inline.
- Pros: tight React integration, can compose with your app, prop-based customization, no cross-origin issues
- Cons: SDK becomes part of your bundle (often 500KB+), styles may bleed or conflict with your CSS, React version compatibility matters
- Who uses this: Metabase Embedding SDK, Cube React Embed SDK, Lightdash SDK, Tableau Embedding API, PowerBI React
This is the most common approach in 2026. Good React integration, but watch out for bundle size and CSS scoping.
3. Web Components (Custom Elements)
The vendor ships HTML custom elements that work in any framework. You use them like <dbn-dashboard token="..." dashboard-id="..."></dbn-dashboard>. Under the hood they render inside Shadow DOM, which gives full style isolation without iframes.
- Pros: Shadow DOM isolates styles (no CSS conflicts ever), works in React, Vue, Angular, plain HTML, Svelte — one integration code path for all frameworks, smaller surface area than full SDKs
- Cons: slightly less idiomatic in strict-TypeScript React (need to declare JSX types), peer-dependency footprint when the component is React-under-the-hood
- Who uses this: Databrain, Embeddable, some newer platforms
This is the approach we'll use in the code below. If your team uses more than one frontend framework, or if you're paranoid about CSS conflicts, web components are the cleanest option.
Step 1: Install the Embed SDK
The code below uses @databrainhq/plugin as a concrete example because it's a web-component embed and shows the pattern cleanly. The integration shape — install, mint a token on the backend, render a component on the frontend, refresh the token before expiry — is near-identical for Metabase SDK, Cube, Lightdash, and Reveal. Swap the install command and the token endpoint for your vendor of choice; the structure transfers.
@databrainhq/plugin ships <dbn-dashboard> and <dbn-metric> as web components:
npm install @databrainhq/plugin
React 19 note: as of @databrainhq/plugin v0.16, the peer dependencies declare React 18. It works with React 19 in practice, but npm may print a peer-dependency warning. Use --legacy-peer-deps or an overrides field in package.json if your CI treats warnings as errors.
Register the custom elements once, usually in your app entry file:
// src/main.tsx
import '@databrainhq/plugin/web';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);
Because dbn-dashboard and dbn-metric aren't native React elements, TypeScript needs a tiny declaration so it stops yelling:
// src/types/dbn.d.ts
declare global {
namespace JSX {
interface IntrinsicElements {
'dbn-dashboard': any;
'dbn-metric': any;
}
}
}
export {};
That's the entire setup. The web component registration happens at import time, so any component anywhere in your tree can now render <dbn-dashboard>.
Step 2: Generate a Guest Token from Your Backend
Never put your API key on the frontend. Every embed SDK works on the same principle: your backend exchanges your long-lived API key for a short-lived "guest token" scoped to a specific user. The frontend only ever sees the guest token.
Here's the token endpoint in Node.js / Express:
// server/routes/guest-token.js
import express from 'express';
const router = express.Router();
router.post('/api/guest-token', async (req, res) => {
const { clientId } = req.body;
const response = await fetch(
`${process.env.DATABRAIN_API_URL}/api/v2/guest-token/create`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.DATABRAIN_API_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
clientId,
dataAppName: process.env.DATABRAIN_DATA_APP_NAME,
permissions: {
isEnableManageMetrics: false,
isEnableCustomizeLayout: false,
isEnableUnderlyingData: false,
isEnableDownloadMetrics: true,
isShowDashboardName: true,
},
}),
}
);
const data = await response.json();
if (response.ok && data.token) {
res.json({ token: data.token });
} else {
res.status(400).json({ error: data?.error?.message || 'Token creation failed' });
}
});
export default router;
Two things to notice:
clientIdis the tenant boundary. Whatever you pass asclientIddetermines what data the token can see. In a SaaS app, pass your own tenant ID or workspace ID here — and make sure every metric definition in your dashboard filters on this field. Miss it in one metric and you've shipped a cross-tenant leak, so treat metric definitions with the same rigor as SQL-injected filters in your main app.- The
permissionsobject is your embed RBAC. Different user roles (viewer, editor, admin) get different guest tokens with different permissions. You still need your own auth to decide which user gets which token — but you don't build a separate permission-check system for the analytics layer.
In production, you'll want to derive clientId from the authenticated user's session — never trust a value sent from the frontend:
router.post('/api/guest-token', authenticate, async (req, res) => {
const clientId = req.user.tenantId;
});
Step 3: The React Component
Now the frontend side — fetch the token, render the dashboard, and refresh the token before it expires (guest tokens typically last 1 hour):
// src/components/AnalyticsDashboard.tsx
import { useEffect, useState, useCallback } from 'react';
interface Props {
dashboardId: string;
tenantId: string;
}
export function AnalyticsDashboard({ dashboardId, tenantId }: Props) {
const [token, setToken] = useState<string | null>(null);
const fetchToken = useCallback(async () => {
const res = await fetch('/api/guest-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ clientId: tenantId }),
});
if (!res.ok) return;
const data = await res.json();
setToken(data.token);
}, [tenantId]);
useEffect(() => {
fetchToken();
const interval = setInterval(fetchToken, 50 * 60 * 1000);
return () => clearInterval(interval);
}, [fetchToken]);
if (!token) return <div>Loading analytics…</div>;
return (
<dbn-dashboard
token={token}
dashboard-id={dashboardId}
enable-download-csv
enable-email-csv
/>
);
}
That's the whole embed. The <dbn-dashboard> element handles chart rendering, drill-downs, cross-filters, CSV export, email export, theming, and real-time data — all configured in the dashboard's definition in Databrain, not in your React code.
Use it the same way you'd use any component:
import { AnalyticsDashboard } from '@/components/AnalyticsDashboard';
export function ReportsPage() {
return (
<div className="p-6">
<h1>Your Reports</h1>
<AnalyticsDashboard
dashboardId="revenue-overview"
tenantId={currentUser.workspaceId}
/>
</div>
);
}
Step 4: Row-Level Security via Guest Tokens
Row-level security is the hardest thing to build from scratch, and the biggest reason teams switch from custom to embedded. Here's why.
In a multi-tenant SaaS, tenant A must never see tenant B's rows, even if they know each other's URLs. Building this yourself requires:
- A tenant-aware middleware on every API route
- WHERE clauses injected into every query (or Postgres RLS policies, if you're careful)
- Audit logs for access attempts
- UI that hides/shows features based on tenant plan
- Integration tests for every tenant-scoping bug you'll create
With embed SDKs, this is reduced to passing the right clientId when minting the token. The token itself carries the scope:
const token = await createGuestToken({
clientId: req.user.tenantId,
});
The analytics platform resolves every query for that token with a WHERE tenant_id = $clientId filter — as long as every metric definition in your dashboard references that field. If one metric skips the filter, that metric leaks across tenants. The platform reduces the surface area a lot, but it doesn't eliminate the need to review metric definitions when you add new ones.
This is also how you do user-level access on top of tenant-level access. Two patterns:
- Soft filtering via token context: pass extra context like
{ userId, role }in the token and have your metric definitions filter on it - Permission flags in the token: the
permissionsobject in the token body controls what features show up (metric creation, layout customization, downloads)
The shorthand: you define your row-scoping rules once in the platform, and every embed for every customer honors them. No leaky middleware, no forgotten WHERE clause.
Step 5: Next.js App Router Integration
If your app is on Next.js 13+ with App Router, two things change:
- The
@databrainhq/plugin/webimport registers custom elements, which requiresdocument— so the embed component must be a Client Component ('use client'). - The token endpoint becomes a Route Handler.
Route Handler (server-side)
// app/api/guest-token/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/lib/auth';
export async function POST(request: NextRequest) {
const session = await auth();
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const response = await fetch(
`${process.env.DATABRAIN_API_URL}/api/v2/guest-token/create`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.DATABRAIN_API_TOKEN}`,
},
body: JSON.stringify({
clientId: session.user.tenantId,
dataAppName: process.env.DATABRAIN_DATA_APP_NAME,
}),
}
);
const data = await response.json();
return NextResponse.json(data);
}
Client Component (browser-side)
// app/components/DatabrainEmbed.tsx
'use client';
import { useEffect, useState } from 'react';
import '@databrainhq/plugin/web';
interface Props {
dashboardId: string;
}
export default function DatabrainEmbed({ dashboardId }: Props) {
const [token, setToken] = useState<string | null>(null);
useEffect(() => {
fetch('/api/guest-token', { method: 'POST' })
.then((res) => res.json())
.then((data) => setToken(data.token));
}, []);
if (!token) return <div>Loading…</div>;
return <dbn-dashboard token={token} dashboard-id={dashboardId} />;
}
Using it in a Server Component
// app/reports/page.tsx
import DatabrainEmbed from '@/app/components/DatabrainEmbed';
export default function ReportsPage() {
return (
<main>
<h1>Reports</h1>
<DatabrainEmbed dashboardId="revenue-overview" />
</main>
);
}
The parent page stays a Server Component — only the embed itself is a Client Component. That keeps your bundle small and your auth server-side. Full working code is at dbn-demo-next-app-router.
Pages Router (if you're still on it)
Pages Router works too — put the token endpoint at pages/api/guest-token.ts and import the embed component with dynamic(..., { ssr: false }):
import dynamic from 'next/dynamic';
const DatabrainEmbed = dynamic(
() => import('../components/DatabrainEmbed'),
{ ssr: false },
);
See the pages-router demo repo for the full setup.
Step 6: Theming to Match Your Brand
Embedded dashboards that don't match your product's look feel like a bolted-on iframe, and users notice. Every embed SDK has a theming layer. Here's the Databrain version:
<dbn-dashboard
token={token}
dashboard-id={dashboardId}
theme={JSON.stringify({
button: {
primary: '#0066CC',
primaryText: '#FFFFFF',
},
drillBreadCrumbs: {
fontFamily: 'Inter, sans-serif',
fontColor: '#374151',
activeColor: '#2563eb',
},
datePickerColor: '#0066CC',
})}
options={JSON.stringify({
chartColors: ['#0066CC', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6'],
})}
/>
A few tips from teams who've shipped this:
- Pull theme tokens from your design system (e.g. your Tailwind config or CSS variables) rather than hardcoding colors in the embed — so brand refreshes don't require redeploying the analytics integration.
- Chart palettes matter more than you think. Default rainbow palettes look generic; a 5–7 color palette derived from your brand makes the embed look native.
- **Shadow DOM means your page CSS can't leak in either.** This is normally a feature, but it means you can't style the embed's internals with global CSS — use the theming API.
Full theming API (chart palettes, breakpoints, metric layouts, font scaling) is documented in the component API reference.
How Embedded Analytics Platforms for React Compare
The React embedded analytics SERP is dominated by five or six platforms with very different shapes. Honest breakdown:
Pricing disclaimer: starts-at numbers are as of April 2026 and move frequently — always cross-check with each vendor's current pricing page before relying on these for a build-vs-buy decision.
Quick picking guide:
- If you want the fastest path to a multi-tenant customer-facing dashboard with many chart types, drill-downs, and exports: Databrain
- If you're already using Metabase internally and want to extend it to customers: Metabase SDK
- If you want a headless semantic layer and are happy to build chart UI yourself: Cube
- If your analytics are driven from dbt and your team lives there: Lightdash
- If you want zero frontend assumptions and maximum style isolation: Databrain or Embeddable (both use web components)
For a direct comparison on specific competitors, see our Power BI Embedded alternatives guide.
What You Get Out of the Box vs. Building It
If you skipped the custom build guide because you've already decided to embed, here's the concrete feature gap you're not rebuilding:
The interesting column isn't time-to-ship — it's maintenance tax. Every one of those capabilities needs ongoing engineering forever. Embedded analytics moves that off your roadmap.
When to Build Anyway
Embedding isn't always right. Build custom when:
- The dashboard is the product. If you're Mixpanel, you don't embed Metabase.
- You need UI that no vendor exposes. Interactive simulations, custom drawing tools, dashboards with real-time collaboration cursors.
- You have an analytics team. If you have three engineers whose full-time job is dashboards, building gives them leverage.
- The data is ultra-sensitive and can't leave your VPC. Many embedded platforms offer self-hosted deployment on Docker/Kubernetes/VMs, but if your compliance posture forbids any vendor anywhere in the data path, build.
If any of those describe you, start with our guide on creating a dashboard in React from scratch — the code is in a runnable starter at github.com/databrainhq/dbn-demos-updated/tree/main/react-tutorial-scratch. And before you commit, work through the build vs. buy cost breakdown — the multi-tenancy and AI-infrastructure cost lines are where homegrown estimates consistently miss.
For everyone else, embed. The labor math is rarely close.
Next Steps
- Clone a working demo: dbn-demo-react for Vite / CRA-style setup, or dbn-demo-next-app-router for Next.js 15
- If you still want to build from scratch: How to create a dashboard in React — a full Vite + React 19 + shadcn + TanStack Query tutorial
- The full build vs. buy math: Embedded analytics build vs. buy — the multi-tenancy and AI costs most teams miss
- What is embedded analytics?: Complete embedded analytics guide — architecture, buyer's checklist, vendor landscape
- See how teams got here: BerryBox tried Power BI, switched to Databrain, saved $250K and 6 months of engineering. Freightify built in-house, then swapped to Databrain in a week. SpotDraft replaced Looker in 4 weeks and saved $300K plus 9 months of engineering.
- Try it free: Start building with Databrain
- Developer docs: docs.usedatabrain.com
Covers React 19, Next.js 15 App Router, @databrainhq/plugin v0.16. Last updated April 2026.
Rahul Pattamatta is co-founder of Databrain, an embedded analytics platform for SaaS.
Frequently Asked Questions
What's the difference between an iframe embed and an SDK embed in React?
Iframes are zero bundle impact and zero integration, but styling and auth are awkward. SDKs become part of your React component tree with props and callbacks but add to your bundle. Web components are the middle ground — framework-agnostic markup with full style isolation via Shadow DOM. For customer-facing SaaS, SDK or web component beats iframe almost every time.
How do I handle authentication for an embedded dashboard in React?
Never put your analytics platform's API key on the frontend. The pattern is: authenticated user hits your backend, your backend exchanges its API key for a short-lived "guest token" scoped to that user (often an hour), frontend receives only the guest token, embed component uses the token to render. Refresh the token before expiry with a 50-minute setInterval.
Can I embed analytics in Next.js App Router?
Yes. Put the token endpoint in a Route Handler (app/api/guest-token/route.ts), put the embed component in a Client Component ('use client'), and use the Client Component from any Server Component page. The 'use client' directive is required because embed SDKs register custom elements at import time, which needs document.
How do I implement row-level security without building it myself?
Every major embed SDK resolves queries in the context of the token — so if the token is scoped to tenantId = 42, every query for that token gets a WHERE tenant_id = 42 filter, provided the metric definition references that field. Configure this once per metric in the dashboard, review every new metric for the same filter, and the platform handles the rest. You still own the discipline of getting metrics right; you don't own the middleware plumbing.
Does embedded analytics work with React Server Components?
Indirectly. The embed itself must be a Client Component because it registers custom elements in the browser. But you can call it from Server Components — just wrap the embed in 'use client' and use it inside an app/page.tsx Server Component. Data-fetching for the embed itself happens client-side (token fetch + the embed's own data requests).




