Skip to main content

Winner of the Embedded Analytics Solution of the Year at the Data Breakthrough Awards 2026

Developer walkthrough

See how Embeddable works

The dashboard your users see, the code behind it, the token that keeps each tenant's data separate, and AI on the same foundation. Scroll to walk through it, or just click around.

Start the story
Act 1 · The dashboard

It starts inside your product

A live dashboard, rendered right in your product by one <em-beddable> web component - any frontend. Built in the no-code builder, or defined as code in your repo: orders.embeddable.yml.

Act 1 · Self-serve: Custom Canvas

Users build their own charts, safely

Add a chart, pick a template, then guided inputs: a dataset, a metric, a dimension - only the fields you’ve exposed. A live preview updates as they go. No ticket, no BI seat.

Custom Canvas docs →
Revenue overview
Acme Corp
Last 90 days ▾All regions ▾+ Filter
measure: total_price - ‘Total USD’
Total USD
$0.0k
+12.4%
# of orders
0
+8.1%
Conversion
0.0%
+0.9pt
Renewals
0%
<BarChart /> + BarChart.emb.ts
Total USD by week
Weekly, last 7 weeks
# of orders trend
Daily, last 90 days
Add a chart +
# of orders by location
Select a template
Build your chart
Dataset to displayRequired
Orders
MetricRequired
# of orders
Slice
Location
Act 2 · Components

Every chart is two files in your repo

A plain React component, plus an .emb.ts companion that defines its inputs and data - that “Dataset to display” field is this array. Start from the built-in suite or write your own; one command - npm run embeddable:push - sends it to your workspace, from your CI too.

Pushing code docs →
Act 2 · Data models

Metrics defined once, next to your schema

Data models are simple YAML files. “# of orders” and “Location” exist in the end-user experience because the model defines them - one governed definition, everywhere.

Act 2 · The semantic layer

Charts ask; the model answers in SQL

Charts send a JSON query; the semantic layer compiles it into your database’s dialect - Postgres, Snowflake, BigQuery, ClickHouse and more - and queries it directly, with caching in front. Already run Cube? Bring your own.

Data modeling docs →
Revenue overview
Acme Corp
Last 90 days ▾All regions ▾+ Filter
measure: total_price - ‘Total USD’
Total USD
$0.0k
+12.4%
# of orders
0
+8.1%
Conversion
0.0%
+0.9pt
Renewals
0%
<BarChart /> + BarChart.emb.ts
Total USD by week
Weekly, last 7 weeks
# of orders trend
Daily, last 90 days
Add a chart +
# of orders by location
import { defineComponent } from '@embeddable.com/react';import { loadData } from '@embeddable.com/core';import Component from './index'; // plain React export const meta = {  name: 'BarChart', label: 'Bar chart',  inputs: [    { name: 'ds', type: 'dataset',      label: 'Dataset to display' },    { name: 'metric', type: 'measure', required: true },    { name: 'slice', type: 'dimension',      config: { dataset: 'ds' } },  ],}; export default defineComponent(Component, meta, {  props: (inputs) => ({ ...inputs,    results: loadData({ from: inputs.ds,      select: [inputs.slice, inputs.metric] }),  }),});
cubes:  - name: orders    sql_table: public.orders     measures:      - name: count        type: count        title: '# of orders'      - name: total_price        title: 'Total USD'        type: sum        sql: price_in_cents / 100.0     dimensions:      - name: location        sql: CONCAT(city, ', ', country)        type: string
// the chart sends a query…{ "query": {    "dimensions": ["orders.location"],    "measures": ["orders.count"],    "limit": 100 } } -- …the semantic layer compiles it-- to SQL in YOUR database's dialect:SELECT  CONCAT(city, ', ', country) "orders__location",  count("orders".id) "orders__count"FROM public.orders AS "orders"GROUP BY 1 ORDER BY 2 DESCLIMIT 100
Act 3 · Multi-tenant

One dashboard, every customer - never cloned per tenant

Your server sends a securityContext - any key-value pairs; tenantId here, but anything your schema filters on - and gets back a short-lived signed JWT the web component attaches to every request. Per-customer databases? Environments handle that.

Act 3 · The switch

Change the context, the data follows

The securityContext flows into every generated query - and with security filters, enforcement is fail-closed: a query that can’t apply its filter errors rather than leak. Same components, same models - Globex’s token can only ever see Globex’s rows.

Tokens API docs →
Revenue overview
Acme Corp
Last 90 days ▾All regions ▾+ Filter
measure: total_price - ‘Total USD’
Total USD
$0.0k
+12.4%
# of orders
0
+8.1%
Conversion
0.0%
+0.9pt
Renewals
0%
<BarChart /> + BarChart.emb.ts
Total USD by week
Weekly, last 7 weeks
# of orders trend
Daily, last 90 days
Add a chart +
# of orders by location
your server → POST api.<region>.embeddable.com
Authorization: Bearer ••••  // your API key, never in the browser
{
  "embeddableId": "orders-dashboard",
  "savedVersion": "production",
  "user": "jess@acme.example",
  // any key-values - match your own schema
  "securityContext": { "tenantId": "acme" }
}
↓ signed JWT → <em-beddable token="eyJh…"/> → every query:
WHERE tenant_id = '{ COMPILE_CONTEXT.securityContext.tenantId }'
WHERE tenant_id = 'acme'
security filters are fail-closed:can’t be applied? the query errors - never unfiltered rows
Act 4 · AI

AI experiences, on the same foundation

One <em-ai-chat> web component answers data questions in natural language - it queries through the same models and metric definitions, so its numbers agree with your dashboards.

Act 4 · Guardrails

Same token, same guardrails

Every answer is scoped by the securityContext: the AI sees only this tenant’s rows and can’t invent metrics you haven’t defined. And it’s your LLM - bring your own provider and key, stored encrypted.

AI chatbot docs →
Revenue overview
Acme Corp
Last 90 days ▾All regions ▾+ Filter
measure: total_price - ‘Total USD’
Total USD
$0.0k
+12.4%
# of orders
0
+8.1%
Conversion
0.0%
+0.9pt
Renewals
0%
<BarChart /> + BarChart.emb.ts
Total USD by week
Weekly, last 7 weeks
# of orders trend
Daily, last 90 days
Add a chart +
# of orders by location
Ask your dataAI
Which region drove revenue growth in April?

Germany - Total USD up 18% month-over-month, led by repeat orders. Top regions:

scoped by securityContext · sees only this tenant’s rows
Ship it

It all ships from your repo

Components, models and dashboards live in version control and ship through your CI: publish a version and the embedded dashboard updates; roll back by repointing "savedVersion".

  1. npm run embeddable:devbuild locally
  2. npm run embeddable:pushbuild + push to your workspace
  3. publish a versiondev · staging · production
  4. the token pins it"savedVersion": "production"

SOC 2 Type II · GDPR · SSO · No iframes - your DOM, your CSS

Ask us