How to Build Customer-Facing Analytics for B2B SaaS
Your B2B customers need access to their data. Here's how to ship governed, multi-tenant analytics using a semantic layer instead of building dashboards from scratch.
Your B2B customers want to see their data. Usage metrics, billing summaries, conversion funnels, performance dashboards. Every customer expects analytics inside your product. They shouldn't have to ask your support team for a CSV export.
The question isn't whether to ship customer-facing analytics. It's how.
Most teams start with one of two approaches. They embed a BI tool (Metabase, Looker, Power BI) and fight with multi-tenancy, iframe styling, and paid embedding licenses. Or they build custom charts from scratch and spend months maintaining SQL queries, API endpoints, and frontend components that nobody asked for.
Both approaches burn engineering time on the wrong problem. You end up building analytics infrastructure instead of your product.
There's a third approach: define your metrics once in a semantic layer, scope them per tenant, and serve them through every surface your customers use. Dashboards, React components, APIs, and AI agents. One definition, every customer, same governed data.
Why embedded BI tools fall short
Embedding a BI tool sounds fast. Drop in an iframe, connect to your database, ship it. In practice, the friction shows up quickly.
Multi-tenancy is an afterthought
Most BI tools were built for internal teams, not B2B products serving hundreds of tenants. Multi-tenancy is either missing, manual, or gated behind an enterprise license.
Metabase requires the Enterprise license ($500+/month) for row-level permissions and sandboxed embedding. The open-source version has basic embedding but no tenant isolation. You end up writing middleware to filter queries by tenant, which is exactly the custom infrastructure you were trying to avoid.
Looker's embedded analytics requires an enterprise contract. Power BI Embedded uses capacity-based pricing that gets expensive at scale. Tableau's embedding story is Salesforce-priced.
Even when multi-tenancy is available, it's usually dashboard-level or role-based, not per-query structural enforcement. You're trusting the BI tool to filter correctly on every query. One misconfiguration and Customer A sees Customer B's data.
Styling and UX limitations
An iframe is a foreign element in your product. It looks like a foreign element. Matching your brand's fonts, colors, spacing, and interaction patterns inside an embedded BI tool ranges from difficult to impossible. Your customers notice.
White-label analytics means your customers shouldn't know they're using a third-party tool. Most embedded BI solutions make this hard. The ones that make it easy charge for it.
You're locked to dashboards
Embedded BI gives you dashboards. That's one surface. But your customers might also want:
- An API to pull metrics into their own tools
- AI agents that answer questions about their data
- Scheduled email reports
- Webhook alerts when metrics cross thresholds
- CSV/Excel exports for their finance team
Each of these requires a different integration, often with different tools and different metric definitions. The dashboard shows one number. The API returns a different one. The export uses yet another query. The metrics drift.
Why building from scratch is worse
The alternative: skip the BI tool and build it yourself. Custom SQL queries, custom API endpoints, custom React charts.
This works for the first dashboard. Then the second. By the tenth, you're maintaining a bespoke analytics platform. Every new metric means a new SQL query, a new API endpoint, a new frontend component, and a new set of tests. Your data engineers are writing API handlers instead of defining metrics. Your frontend engineers are debugging chart edge cases instead of building product features.
The real cost isn't the initial build. It's the ongoing maintenance:
- Metric drift. Revenue is calculated differently in the dashboard, the API, and the export. Nobody notices until a customer complains.
- No caching layer. Every API call hits the warehouse. Response times grow with data volume. Your customers experience slow dashboards at month-end when they need them most.
- Security is DIY. You build tenant filtering yourself. You test it yourself. You hope you didn't miss an edge case.
- No schema evolution. Adding a new dimension means updating every query, endpoint, and component that touches the affected metric. A one-line change in business logic cascades into a multi-day project.
The teams on Reddit asking "Is embedded analytics for SaaS actually worth it vs building your own charts?" are wrestling with exactly this tradeoff. The answer is neither. The right approach is a governed metrics layer.
The semantic layer approach
Instead of embedding a BI tool or building from scratch, define your metrics once in a semantic layer and serve them to every surface.
Here's how this works end to end.
Define your metrics
cubes:
- name: usage_events
sql_table: public.events
measures:
- name: total_events
type: count
- name: unique_users
sql: user_id
type: count_distinct
- name: api_calls
sql: "CASE WHEN event_type = 'api_call' THEN 1 ELSE 0 END"
type: sum
dimensions:
- name: event_type
sql: event_type
type: string
- name: tenant_id
sql: tenant_id
type: string
- name: created_at
sql: created_at
type: time
security_context:
- name: tenant_filter
sql: "{SECURITY_CONTEXT.tenant_id} = tenant_id"
Three things to note:
- Metrics are defined once.
total_events,unique_users, andapi_callshave fixed definitions. Every surface that queries them gets the same number. - Multi-tenancy is structural. The
security_contextfilter runs on every query. Customer A's publishable key carriestenant_id = customer_a. The semantic layer appendsWHERE tenant_id = 'customer_a'to every query automatically. The consumer can't skip it. - It's YAML in Git. Change a metric definition in a pull request. Review it. Merge it. Every consumer gets the update on the next
bon deploy.
Add caching for production performance
Your customers don't want to wait 8 seconds for a dashboard to load. Pre-aggregation caches the most common queries:
pre_aggregations:
- name: daily_usage
measures:
- total_events
- unique_users
- api_calls
dimensions:
- event_type
time_dimension: created_at
granularity: day
refresh_key:
every: 1 hour
Hot queries hit the cache. Single-digit millisecond responses. Cold queries fall through to the warehouse. Your customers get fast dashboards. Your warehouse doesn't melt at month-end.
This matters more than it seems. Slow analytics is a churn signal. If your customer's team stops opening the analytics tab because it takes too long, they stop seeing value. Fast dashboards get used. Used dashboards prove ROI. Proven ROI renews contracts.
Authenticate per tenant
Two patterns depending on your architecture:
Publishable keys for simple cases. Create one via the dashboard (Settings > API Keys) or CLI (bon keys create). Each key scopes all queries to a tenant's data. Pass it to your frontend. Good for demos and single-tenant setups.
Token exchange for production multi-tenancy. Your server uses a secret key (bon_sk_...) to mint a scoped JWT for each customer session:
// Your server: exchange secret key for a tenant-scoped token
const res = await fetch("https://app.bonnard.dev/api/sdk/token", {
method: "POST",
headers: {
Authorization: "Bearer bon_sk_...",
"Content-Type": "application/json",
},
body: JSON.stringify({
security_context: { tenant_id: "customer-123" },
}),
});
const { token } = await res.json();
// Return this token to your frontend
// Your frontend: use the scoped token
import { createClient } from "@bonnard/sdk";
const bon = createClient({
fetchToken: async () => {
const res = await fetch("/api/analytics/token");
const { token } = await res.json();
return token;
},
});
Tokens are cached automatically and refreshed 60 seconds before expiry. The secret key never leaves your server. The frontend token carries the tenant's security context, and every query is automatically scoped. No filtering code in your application.
Keys and tokens can be rotated or revoked without redeploying your schema. If a customer churns, revoke their key. If a key is compromised, rotate it. No code changes.
Serve through every surface
One bon deploy and your metrics are available through:
React components in your product. Drop charts into your frontend with the React SDK:
import { BonnardProvider, BarChart, useBonnardQuery } from "@bonnard/react";
import "@bonnard/react/styles.css";
function App() {
return (
<BonnardProvider config={{ apiKey: "bon_pk_..." }}>
<CustomerDashboard />
</BonnardProvider>
);
}
function CustomerDashboard() {
const { data, loading } = useBonnardQuery({
query: {
measures: ["usage_events.total_events"],
dimensions: ["usage_events.event_type"],
timeDimensions: [{
dimension: "usage_events.created_at",
dateRange: "last 30 days",
}],
},
});
if (loading || !data) return <p>Loading...</p>;
return <BarChart data={data} x="usage_events.event_type" y="usage_events.total_events" />;
}
No iframe. Native React components styled with your design system. Wrap your app in BonnardProvider with the API key (created in Settings > API Keys in the Bonnard dashboard), use useBonnardQuery to fetch governed data, and pass it to chart components. Available components: BigValue, BarChart, LineChart, AreaChart, PieChart, DataTable, DashboardViewer (for embedding full markdown dashboards), BonnardChart (a universal renderer that takes a spec object and renders any chart type), and the useBonnardQuery hook.
Styling is handled through CSS custom properties (--bon-bg, --bon-text, --bon-border, --bon-radius), so charts match your product's design system without overriding internals. Pass a palette prop on BonnardProvider to set chart colors: choose from built-in palettes (default, tableau, observable, metabase) or pass a custom array.
AI agents via MCP. Give your customers a publishable key for their MCP-compatible AI agent (Claude, Cursor, etc.). They connect to your analytics the same way they connect to any MCP tool. They ask "What's my API usage this month?" and get governed data scoped to their tenant.
Cloud (OAuth 2.0 with PKCE, scoped to the customer's tenant):
{
"mcpServers": {
"acme-analytics": {
"type": "http",
"url": "https://analytics.acme.com/mcp"
}
}
}
Self-hosted (Bearer token):
{
"mcpServers": {
"acme-analytics": {
"url": "https://analytics.acme.com/mcp",
"headers": {
"Authorization": "Bearer bon_pk_abc123"
}
}
}
}
Your customer's agent calls explore_schema, sees their available metrics, and queries them. Multi-tenancy is handled by the publishable key. No custom integration on your side.
REST API for custom integrations. Some customers want to pull metrics into their own tools. The REST API returns the same governed data:
curl https://analytics.acme.com/v1/query \
-H "Authorization: Bearer bon_pk_abc123" \
-d '{"measures": ["usage_events.api_calls"], "timeDimensions": [{"dimension": "usage_events.created_at", "granularity": "day", "dateRange": "last 30 days"}]}'
Markdown dashboards for simple use cases. Author dashboards in plain text, deploy with bon deploy, scoped per tenant with publishable keys. No frontend code needed.
Every surface queries the same metric definitions. Every surface respects the same access controls. The number your customer sees in a React chart matches what their AI agent returns, which matches what the REST API outputs. One source of truth.
Build vs. buy vs. semantic layer
| Build from scratch | Embed BI tool | Semantic layer | |
|---|---|---|---|
| Time to first dashboard | 2-4 months | 1-2 weeks | 1-2 weeks |
| Multi-tenancy | DIY (error-prone) | Paid license or manual | Built-in, structural |
| Metric consistency | Drifts across endpoints | Single tool only | Guaranteed across all surfaces |
| Surfaces | Only what you build | Dashboards only | Dashboards + API + SDK + AI agents |
| Caching | DIY or none | Tool-dependent | Pre-aggregation, configurable |
| Maintenance burden | High (you own everything) | Medium (tool updates, iframes) | Low (schema changes only) |
| AI agent support | Build from scratch | None | MCP, native |
| Customer self-serve | Limited to what you build | Limited to dashboard features | Full (agents, API, dashboards) |
| Cost at scale | Engineering time | License fees per user/capacity | Open source or usage-based |
What makes this different from embedded analytics vendors?
Embedded analytics tools (Metabase Embedded, Holistics, Explo, Luzmo, Reveal) give you dashboards inside your product. That's their scope. They're good at it.
A semantic layer approach is different in three ways:
1. Multiple surfaces, not just dashboards. Your customers don't just want charts. They want APIs, AI agents, exports, and integrations. An embedded BI tool gives you one surface. A semantic layer gives you all of them from the same definitions.
2. The governance is the product. Embedded BI tools enforce access control at the dashboard level. A semantic layer enforces it at the query level. Every query, every surface, every consumer. The security model is deeper and more reliable.
3. AI agents are first-class. No embedded BI tool has MCP support. None of them are designed for agent-scale query volumes. If your customers want to connect AI agents to their data (and increasingly they do), embedded BI tools can't serve that use case. A semantic layer with MCP support can.
The tradeoff: embedded BI tools have richer out-of-the-box visualization. If your customers need drag-and-drop chart builders and you don't want to build any frontend, an embedded BI tool is faster to ship. If you want control over the UX and need to serve multiple surfaces, the semantic layer approach wins.
Real-world architecture
Here's what this looks like in a production B2B SaaS product.
Your SaaS Product
├── Frontend (React)
│ ├── Customer Dashboard (Bonnard React SDK + publishable key)
│ └── Internal Analytics (Bonnard React SDK + admin key)
├── Backend
│ ├── Scheduled Reports (REST API + per-tenant keys)
│ └── Webhook Alerts (query API on schedule, trigger on threshold)
└── Customer Integrations
├── MCP Server (customers connect AI agents with publishable keys)
└── REST API (customers pull data into their own tools)
Bonnard Semantic Layer
├── Schema (YAML in Git)
│ ├── Cubes (metrics, dimensions, joins)
│ ├── Views (curated interfaces per consumer)
│ ├── Security Context (tenant isolation)
│ └── Pre-aggregations (caching rules)
├── Connected to: Snowflake / BigQuery / Postgres
└── Serves: React SDK, MCP, REST API, TypeScript SDK
One schema definition powers your entire analytics surface area. Add a new metric and every surface gets it. Fix a calculation and every customer sees the correction. Revoke a key and that customer loses access. No code deployment needed for any of it.
When to use this approach
Use the semantic layer approach when:
- You're a B2B SaaS product shipping analytics to multiple customers
- You need multi-tenant isolation on every query
- Your customers want more than dashboards (APIs, AI agents, exports)
- You want consistent metrics across internal and customer-facing surfaces
- Your data team is tired of being a service desk for ad-hoc metric requests
Use embedded BI when:
- You need drag-and-drop chart building for non-technical customers
- Dashboards are the only surface you need
- You're in one BI ecosystem and don't need API or agent access
- Budget is limited and you don't need multi-tenancy
Build from scratch when:
- Your analytics use case is truly unique and no tool covers it
- You have a dedicated analytics engineering team with time to maintain custom infrastructure
- You need visualizations that no existing tool supports
Most B2B SaaS teams we talk to have tried option 2 (embed BI) or option 3 (build from scratch) and are looking for something better. The semantic layer gives them governed metrics across every surface without the maintenance burden of custom code or the limitations of embedded dashboards.
Getting started
Cloud:
npm install -g @bonnard/cli
bon init
bon deploy
Self-hosted:
npm install -g @bonnard/cli
npx @bonnard/cli init --self-hosted
docker compose up -d
bon deploy
bon init also generates agent configs for Claude Code, Cursor, and Codex so AI coding assistants understand your semantic layer from the first prompt. Define your metrics, create a publishable key in Settings > API Keys, and drop a React chart into your product. Run bon mcp to output MCP connection configs for your customers' AI agents. Use bon diff to preview schema changes before deploying and bon schema to explore your deployed measures and dimensions from the CLI. The full tutorial: How to Connect an AI Agent to Your Data Warehouse.
For background on semantic layers: What Is a Semantic Layer?. For the AI agent angle: What Is an Agentic Semantic Layer?.
Self-host free under Apache 2.0, or use Bonnard Cloud for managed infrastructure.
Frequently asked questions
What is customer-facing analytics?
Customer-facing analytics is analytics embedded in your product for your customers to use. Instead of internal dashboards for your team, the analytics are exposed to end users: your B2B customers, their teams, and their tools. The key challenges are multi-tenancy (each customer sees only their data), performance (customers expect fast load times), and consistency (the numbers should match across every surface).
What is embedded analytics?
Embedded analytics means integrating analytics capabilities directly into another application. This can be as simple as an iframe embedding a dashboard or as sophisticated as native React components querying a governed API. The term covers a range of approaches from basic chart embedding to full white-label analytics platforms.
How is this different from Metabase embedding?
Metabase offers embedded dashboards via iframe or full-app embedding. The open-source version has basic embedding but no tenant isolation. The Enterprise version ($500+/month) adds row-level permissions and sandboxed embedding. The semantic layer approach provides structural multi-tenancy on every query, serves multiple surfaces (not just dashboards), and supports AI agents via MCP. Metabase is a good tool for internal dashboards. For customer-facing B2B analytics at scale, the semantic layer approach gives you more control and better governance.
What is white-label analytics?
White-label analytics means your customers see your brand, not a third-party tool's brand. No "Powered by Metabase" footer. No foreign-looking iframe. The analytics feel native to your product. With a semantic layer approach using React SDK components, the charts are native React components styled with your design system through CSS custom properties (--bon-bg, --bon-text, --bon-border, --bon-radius) and configurable color palettes. There's nothing to white-label because there's no third-party UI.
How does multi-tenancy work?
Each customer gets a publishable key that carries their tenant ID. Every query through that key automatically includes a filter scoping data to that tenant. This is enforced at the semantic layer level, not the application level. The consumer can't bypass it. You don't write filtering code. The security context in your YAML schema defines the rule once, and it applies to every query through every surface.
Can my customers connect their own AI agents?
Yes. Give them a publishable key and MCP server connection details. Their AI agent (Claude, Cursor, or any MCP-compatible client) connects to your analytics and queries governed metrics scoped to their data. They ask "What's my API usage this month?" and get the correct answer, filtered to their tenant. See MCP for how the protocol works.
What warehouses are supported?
Snowflake, BigQuery, Databricks, PostgreSQL (including Supabase, Neon, and RDS), Redshift, and DuckDB (including MotherDuck). The semantic layer generates the appropriate SQL dialect for your warehouse. Swap warehouses without changing your metric definitions or customer integrations.
How much does it cost?
Bonnard is open source under Apache 2.0. Self-host with all features for free. Bonnard Cloud is usage-based starting at $149/month for managed infrastructure. No per-user or per-customer pricing. Your costs don't scale with the number of customers you serve.
Ready to ship a customer-ready MCP?
Turn your semantic layer, dbt, or warehouse into a governed, per-customer MCP for your customers' agents.