Embedded Analytics in Python: A Developer's Guide (2026)
Embed dashboards in Python apps with FastAPI, Django, or Flask. Guest-token pattern for multi-tenant row-level security, three frontend rendering options (web component, Streamlit iframe, Dash iframe), and how Databrain, Metabase, Cube, Superset, and Lightdash compare for Python teams.
.png)
Key Takeaways
- Embedding analytics into a Python app from a FastAPI / Django / Flask backend is a 1–5 day project; building the same capability in Streamlit + custom auth + multi-tenancy is 4–8 weeks of plumbing.
- The integration shape is identical across vendors: your Python backend exchanges a long-lived API key for a short-lived guest token scoped to a tenant ID; the frontend renders a component (web component, JS SDK, or iframe) using only the guest token.
- The frontend does not have to be React. Web components (
<dbn-dashboard>) work in plain HTML, inside a Streamlit app viast.components.v1.html, inside a Dash app viahtml.Iframe, or inside any framework you choose. - Token-scoped row-level security is the killer feature: pass
clientIdwhen minting the token, every query for that token gets aWHERE tenant_id = $clientIdfilter - provided you've defined your metrics correctly. This eliminates 90% of the "build multi-tenancy" plumbing. - Build vs. embed in Python is asymmetric. Streamlit and Dash are excellent for internal dashboards (one tenant, one team) but a poor fit for customer-facing multi-tenant analytics. The crossover point comes faster than most teams expect.
Most Python-backed SaaS products eventually need customer-facing analytics - a dashboard inside the product where each tenant sees their own data, can filter it, drill in, and export. The honest engineering math: building this from scratch in Streamlit or Dash takes 4–8 weeks once you account for multi-tenancy, RBAC, exports, scheduled email reports, theming, and the long tail of "can you also add a Sankey?". Embedding a pre-built analytics layer is a 1–5 day project from a Python backend.
This guide covers what "embedded analytics in Python" actually looks like in 2026: the three integration patterns from a Python backend, what the FastAPI / Django / Flask code looks like end-to-end, how guest tokens give you multi-tenant row-level security without you building it, three frontend rendering options (web component, Streamlit iframe, Dash iframe), and an honest comparison of the platforms Python teams actually use - Databrain, Metabase, Cube, Superset, Preset, Lightdash.
When to Embed vs. Build From Scratch
Before any code, the honest decision:
Build custom when:
- The dashboard is internal only (no customer access, no multi-tenancy, no exports needed)
- You have a dedicated analytics team and a multi-year horizon
- The dashboard is the product itself (you're Mixpanel, Amplitude, Observable)
Embed when:
- Analytics is a feature inside a broader SaaS 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 email reports, drill-downs, and filter bars out of the box
- You want to ship this quarter, not next year
Most Python-backed SaaS teams we've worked with started by building in Streamlit or Dash, hit the multi-tenancy wall, then switched to embedding. The maintenance cost of 20+ chart types with drill-downs and per-tenant access control was 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.
The Three Architectural Patterns
There are three ways analytics platforms integrate into a Python-backed app. The Python piece is the same in all three (your backend mints a token); the difference is how the frontend renders the dashboard.
1. iframes (Streamlit / Dash apps embedded as iframes)
Your existing Streamlit or Dash app, embedded in your customer-facing web app via <iframe src="https://your-streamlit-app.com/?embed=true&tenant=42">.
- Pros: zero frontend integration; full style isolation; trivial to implement; reuses Streamlit/Dash skills your data team already has.
- Cons: awkward auth (cross-origin cookies, token-in-URL is leak-prone); can't style beyond what Streamlit/Dash exposes; resizing is fiddly; breaks down at multi-tenant scale because each request still hits your Streamlit/Dash backend.
- Who uses this: internal teams that have a Streamlit dashboard they want to "expose to customers" without rebuilding it.
iframe-embedding a Streamlit app is fine for internal use. For customer-facing SaaS at any scale, you usually want one of the other two.
2. Hosted analytics URL (Metabase, DataBrain hosted dashboard URL)
The vendor hosts the dashboard; your Python backend mints a signed URL scoped to the tenant; you embed via <iframe src="https://analytics.vendor.com/embed/dashboards/123?token=...">.
- Pros: zero infrastructure on your side; multi-tenancy via the signed URL; works with any frontend.
- Cons: still iframe-based (same UX caveats); per-vendor signing convention you have to follow exactly.
- Who uses this: Metabase Pro / Cloud, Looker Studio, Superset Embedded.
3. Web component served from a Python backend (Databrain <dbn-dashboard>, Embeddable)
The vendor ships an HTML custom element. Your Python backend mints a guest token. The frontend renders <dbn-dashboard token="..." dashboard-id="..."> - this works in plain HTML, inside Streamlit, inside Dash, or inside any framework.
- Pros: Shadow DOM isolates styles (no CSS conflicts); works in React, Vue, Streamlit, Dash, plain HTML - one integration code path; smaller surface area than full SDKs; per-tenant auth via the token, not the URL.
- Cons: slightly less idiomatic in heavily-typed 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 builds in more than one frontend framework - or if your "frontend" is sometimes a Streamlit app and sometimes a customer-facing React app - web components are the cleanest option because the Python backend is the same code in every case.
Step 1: The Databrain API Contract (Same Across Vendors in Spirit)
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 tenant. The frontend only ever sees the guest token. The Databrain endpoint is POST /api/v2/guest-token/create; Metabase, Cube, and Lightdash have analogous endpoints. The Python code for all of them follows the same shape - only the URL and the body keys differ.
The Databrain request body:
clientIdis the tenant boundary. Whatever you pass here determines 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. Treat metric definitions with the same rigor as SQL parameter binding in your main app.permissionsis your embed RBAC. Different user roles (viewer, editor, admin) get different guest tokens with different permissions.expiryTimein seconds (default 3600 = 1 hour). The frontend refreshes the token before expiry.
The response:
Now let's wire this from three Python backends.
Step 2a: FastAPI Guest-Token Endpoint
Run with uvicorn main:app --reload. The endpoint at POST /api/guest-token returns a token scoped to the authenticated tenant - that's the entire backend integration.
Three things to notice:
- Tenant ID comes from the auth dependency, never from the request body. This is the single most important security rule when embedding analytics. Trust your own auth, not anything the frontend sends.
- The Databrain API token lives in environment variables. Never expose it to the frontend.
- The
httpx.AsyncClientis shared across all requests vialifespan. Most FastAPI tutorials showasync with httpx.AsyncClient(...) as client:inside the handler, which spins up a fresh TCP connection pool on every call - fine for tutorials, a footgun in production. The lifespan pattern above is the same code with the pool reused.
Step 2b: Django Guest-Token View
URL config:
Step 2c: Flask Guest-Token Route
Three frameworks, identical contract. The hard part of "embedded analytics in Python" is not Python - it's defining your metrics so they all filter on the tenant ID. The Python backend is ~25 lines.
Step 3: 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 Streamlit / Dash to embedded.
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 in Python means:
- A tenant-aware middleware on every API route
- WHERE clauses injected into every query (or PostgreSQL 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:
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 (Databrain supports this viaparams) and have your metric definitions filter on it. - Permission flags in the token: the
permissionsobject controls what features show up (metric creation, layout customisation, 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 across 30 routes.
Step 4: Three Frontend Rendering Options
The Python backend is the same; the frontend depends on what you're rendering inside.
Option A: Plain HTML (no JS framework)
The simplest path. Works in any HTML page - Jinja, Django templates, FastAPI's Jinja support, even a static HTML file.
That's the entire frontend. No npm install, no React, no build step. Works in Django templates, Flask Jinja, or FastAPI's Jinja2Templates.
Option B: Inside a Streamlit app
If your customer-facing app is itself a Streamlit dashboard (not common, but happens for AI / data-team tools that started internal and grew customer-facing), you can embed via st.components.v1.html:
The token is minted server-side by your Python backend, passed to Streamlit, then handed off to the web component. Same auth model as Option A.
Option C: Inside a Dash app
For Dash, use html.Iframe pointing at a small HTML page your Python backend serves with the web component already wired up. This is the path the Dash maintainers themselves recommend - there's no first-party way to render arbitrary HTML inside a Dash component, and the long-standing third-party workaround (dash-dangerously-set-inner-html) has been archived since 2018 and shouldn't be used in new code.
The /embed/dashboard endpoint is a tiny route in the same Flask app underneath Dash (app.server.route("/embed/dashboard")) that returns the same HTML from Option A - <script> tag plus <dbn-dashboard> element. The iframe boundary keeps the web component's runtime out of Dash's React tree and avoids the deprecated raw-HTML path entirely.
Don't use dash-dangerously-set-inner-html. PyPI marks it as archived; the last release was 0.0.2 in December 2018. Older Dash tutorials still reference it - those tutorials are stale.
Step 5: Triggering Dashboard Refreshes from a Python Data Pipeline
If your data lands via Airflow / Prefect / Dagster, you can poke the analytics layer to refresh after each pipeline run:
Same pattern for Prefect tasks (@flow + @task), Dagster ops, or a plain cron + python script.py. The point: the Python backend that mints tokens is the same Python codebase that owns the data pipeline, so you can wire them together cleanly.
How Embedded Analytics Platforms for Python Compare
The embedded-analytics-from-Python market in 2026 is a small handful of 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, exports, and frontend-framework flexibility (web component works in HTML, Streamlit, Dash, React): Databrain
- If you're already using Metabase internally and want to extend to customers: Metabase Embedding
- If your team has already invested in Cube as a semantic layer: Cube (and build the chart UI yourself)
- If you want pure OSS with no vendor lock-in and you're willing to do the multi-tenancy plumbing: Superset
- If you want managed Superset with embedded guest tokens out of the box: Preset
- If your analytics are driven from dbt: Lightdash
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 in Streamlit / Dash / Gradio when:
- The dashboard is internal only. No customers, no multi-tenancy, no exports, no RBAC. Streamlit ships in a day.
- **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 (Databrain self-hosted, Metabase Enterprise on-prem, Superset OSS), 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 Python from scratch - the runnable code is at github.com/databrainhq/dbn-demos-updated/tree/main/python-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 Python dashboard estimates consistently miss.
For everyone else, embed. The labor math is rarely close.
Next Steps
- The build path: Python Dashboard: The Complete 2026 Guide - full Streamlit + Dash + Gradio walkthroughs, runnable code at python-tutorial-scratch
- Streamlit vs Dash, head-to-head: Streamlit vs Dash in 2026 - same dashboard built in both
- Pick a chart library: 10 Best Python Chart Libraries for Dashboards - Plotly, Matplotlib, Bokeh, Altair, ECharts compared
- 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 FastAPI, Django, Flask, httpx, Databrain API v2 guest-token endpoint. Last updated April 2026.
Rahul Pattamatta is co-founder of Databrain, an embedded analytics platform for SaaS.
Frequently Asked Questions
Can I embed a Streamlit dashboard in my SaaS app?
Yes via iframe (<iframe src="https://your-streamlit-app.com/?embed=true">), but the constraints are real: cross-origin auth is awkward, you can't style beyond what Streamlit exposes, resizing is fiddly, and multi-tenancy means you need a tenant param in the URL - which is leak-prone. For internal use, fine. For customer-facing SaaS analytics, an embedded analytics platform with token-scoped multi-tenancy beats a Streamlit iframe almost every time.
How do I add multi-tenancy to a Python dashboard?
If you're building from scratch in Streamlit / Dash, you have to wire it yourself: tenant-aware middleware on every route, WHERE clauses injected into every query, audit logs, integration tests. Budget 2–4 weeks plus a permanent maintenance tax. If you embed, multi-tenancy is one parameter (clientId) in the guest-token request - the analytics platform handles the rest. See the "Row-Level Security" section above.
Does this work with Django?
Yes - the Django guest-token view is in Step 2b above. Use @login_required as your auth dependency, resolve tenant_id from request.user, and call the same POST /api/v2/guest-token/create endpoint. The frontend (whether your Django templates serve HTML, or you have a separate React frontend) renders the web component using only the returned token.
Do I need React to embed analytics in a Python app?
No. Web component embeds (<dbn-dashboard>) work in plain HTML, inside Django/Flask/Jinja templates, inside a Streamlit app via st.components.v1.html, inside a Dash app via an html.Iframe pointing at a tiny route in your backend that serves the component, or inside Vue / Angular / Svelte. The Python backend is the same in every case.
Can I generate guest tokens from FastAPI?
Yes - the FastAPI guest-token endpoint is in Step 2a above. Use a shared httpx.AsyncClient (instantiated in FastAPI's lifespan and reused across requests - not a fresh async with httpx.AsyncClient(...) per call, which exhausts file descriptors at scale), your existing auth dependency to resolve tenant_id, and POST /api/v2/guest-token/create against the analytics API. ~30 lines including the lifespan setup.




