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 storyIt 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.
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 →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.
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.
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 →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
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.
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 →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" } }
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.
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 →Germany - Total USD up 18% month-over-month, led by repeat orders. Top regions:
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".
npm run embeddable:devbuild locallynpm run embeddable:pushbuild + push to your workspacepublish a versiondev · staging · productionthe token pins it"savedVersion": "production"
SOC 2 Type II · GDPR · SSO · No iframes - your DOM, your CSS