Embed analytics — SDK + session route (web component)
Production path for static HTML, Svelte, Lit, or any host that can load a custom element:
your session-route BFF mints sessions; the page mounts
<embedded-canvas-analytics>.
ec_live_… off the client, binds customerId to your auth session, and is the
production-safe path for SaaS embeds.
Part I — Embedded Canvas setup
Control plane · do this first
EC-1. Sign in and open Analytics
- Sign in at
https://embeddedcanvas.com→ open Analytics.
EC-2. Step 1 — Start analytics runtime
- Provision runtime until Status: active → Launch analytics builder.
EC-3. Step 2 — Configure workspace
- Connect warehouse → set Tenant column → Save.
- Confirm two test tenant ids with different row counts.
EC-4. Build dashboard + native filters
- Create a dataset/chart that changes between tenants → publish the dashboard.
-
Add native filters in the builder for every column you will drive from your app UI later
(tutorial examples:
city,service_line, optionaltech_name). These native filters are required even if you hide the filter bar — host-owned filters sync to them viafilters.extra(see Host-owned filters). - For a white-label product UX, hide the native filter bar in the iframe and own filters in your app UI (Part II).
EC-5. Register dashboard & dual allowlist
- Builder → Embed dashboard → Allowed Domains:
http://localhost:5173. - Analytics Step 3 → register → iframe parent origins:
http://localhost:5173. - Copy the control-plane embed token UUID — you will paste it into Vite
.envasVITE_*(or HTML attributes) in Part II.
EC-6. Step 4 — Create embed API key
- Create embed API key → copy
ec_live_…once and keep it for Part II (you will create the server env file there). - Never put this key in Vite/React/Next public env (
VITE_*/NEXT_PUBLIC_*), browser code, or git.
Values checklist
| Value | Where you get it | Where you put it |
|---|---|---|
EMBED_TOKEN | Analytics Step 3 — dashboard embed token UUID | Client (and server mint body on BFF) |
CUSTOMER_ID | Tenant id from your warehouse / auth | Server derives; client may demo via input |
EMBED_API_KEY | Analytics Step 4 — ec_live_… | Server .env only |
EMBEDDED_CANVAS_API_URL | https://api.embeddedcanvas.com | Server .env only |
Where to put each credential
Copy values from the Analytics wizard into the files below. Secrets stay on the server; the browser only gets the public dashboard embed token + short-lived session from your BFF.
| Credential | Looks like | Put it here | Never put it here |
|---|---|---|---|
EMBED_TOKEN |
UUID from Analytics Step 3 |
Server: BFF .env next to server.mjs as EMBED_TOKEN (session mint body)Browser: Vite .env as VITE_* (or HTML attributes) / embed-token / customer-id on the custom element as embedToken (or NEXT_PUBLIC_EMBED_TOKEN / VITE_EMBED_TOKEN)
|
— |
EMBED_API_KEY |
ec_live_… from Analytics Step 4 |
Server only: BFF .env next to server.mjs → read in your BFF; send as Authorization: Bearer … |
Vite/React client code, NEXT_PUBLIC_*, browser localStorage, repo commits |
EMBEDDED_CANVAS_API_URL |
https://api.embeddedcanvas.com |
Server only: BFF .env next to server.mjs |
Client bundles (unless you only proxy via BFF — still prefer server) |
CUSTOMER_ID / customerId |
Tenant key from your auth / warehouse | Resolved in the BFF from the signed-in user, then returned/used in session mint. Demo UI may use a test input. | Hard-coded production tenant ids that customers can spoof without auth checks |
Session guestToken |
Short-lived token from BFF response | Memory only — SDK receives it via fetchSession; do not persist long-term |
Logs, analytics tools, permanent cookies |
ec_live_… unlocks minting on the server.
EMBED_TOKEN selects which dashboard to open (safe in the client).
customerId selects which tenant rows the guest token may see.
Part II — HTML web component + session route (BFF)
Your laptop · follow in order
You will create a Vite vanilla app (no React/Vue) and a Node
server.mjs BFF in the same folder. The browser never sees ec_live_….
1. Scaffold Vite vanilla + install SDKs
In a terminal, run:
npm create vite@latest my-analytics-html -- --template vanilla
cd my-analytics-html
npm install
npm install @embeddedcanvas/embed-sdk@^1.8.3 @superset-ui/embedded-sdk
Stay in my-analytics-html for every remaining step.
Vite’s vanilla template gives you index.html and src/main.js — no framework required.
node --env-file=.env works when you start the BFF.
Check with node -v.
2. Create server.mjs (the BFF)
This file does not exist yet — Vite does not create it.
In the project root (next to package.json), create server.mjs.
- Create the file:
touch server.mjs/ PowerShellNew-Item server.mjs/ New File in your editor. - Paste the entire contents below and save (do not leave the file empty — empty → HTTP 405).
/**
* Session-route BFF — mints embed sessions with your embed API key.
* Run from this folder: npm run bff
* (uses Node's --env-file=.env; requires Node 20+)
*/
import http from "node:http";
const API_URL = (process.env.EMBEDDED_CANVAS_API_URL || "").replace(/\/$/, "");
const EMBED_API_KEY = process.env.EMBED_API_KEY?.trim();
const EMBED_TOKEN = process.env.EMBED_TOKEN?.trim();
const PORT = Number(process.env.PORT || 3090);
function json(res, status, body) {
res.writeHead(status, {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
});
res.end(JSON.stringify(body));
}
const server = http.createServer(async (req, res) => {
if (req.method === "OPTIONS") return json(res, 204, {});
if (req.method === "POST" && req.url === "/api/analytics/session") {
if (!API_URL || !EMBED_API_KEY || !EMBED_TOKEN) {
return json(res, 500, { error: "Configure .env (EMBEDDED_CANVAS_API_URL, EMBED_API_KEY, EMBED_TOKEN)" });
}
let body = "";
for await (const chunk of req) body += chunk;
let customerId = process.env.TEST_CUSTOMER_ID || "CUST-1004";
let filters;
try {
const parsed = body ? JSON.parse(body) : {};
if (typeof parsed.customerId === "string" && parsed.customerId.trim()) {
customerId = parsed.customerId.trim();
}
filters = parsed.filters;
} catch {
return json(res, 400, { error: "Invalid JSON" });
}
const payload = { embedToken: EMBED_TOKEN, customerId };
if (filters) payload.filters = filters;
const upstream = await fetch(`${API_URL}/public/embed/v1/sessions`, {
method: "POST",
headers: {
Authorization: `Bearer ${EMBED_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
return json(res, upstream.status, await upstream.json());
}
json(res, 404, { error: "Not found" });
});
server.listen(PORT, () => {
console.log(`BFF listening on http://localhost:${PORT}`);
console.log(`POST http://localhost:${PORT}/api/analytics/session`);
});
3. Create project-root .env (paste tokens)
Create one .env next to server.mjs and package.json.
Both the BFF and Vite read from it (Vite only injects names that start with VITE_).
- Create the file:
touch .env(or New File →.env). - Paste the template below.
- Replace the placeholders with the values you copied in Part I:
EMBED_API_KEY← Step 4ec_live_…EMBED_TOKENandVITE_EMBED_TOKEN← paste the same Step 3 UUID into both linesTEST_CUSTOMER_IDandVITE_TEST_CUSTOMER_ID← same warehouse tenant id (example:co_alltrade)
- Save. Confirm
.envis listed in.gitignore.
# Server only — read by: npm run bff (node --env-file=.env server.mjs)
EMBEDDED_CANVAS_API_URL=https://api.embeddedcanvas.com
EMBED_API_KEY=ec_live_PASTE_STEP4_KEY
EMBED_TOKEN=PASTE_STEP3_EMBED_TOKEN_UUID
PORT=3090
TEST_CUSTOMER_ID=CUST-1004
# Same Step 3 UUID again — Vite only injects names that start with VITE_ into src/
VITE_EMBED_TOKEN=PASTE_STEP3_EMBED_TOKEN_UUID
# Same tenant as TEST_CUSTOMER_ID — main.js sets customer-id from this
VITE_TEST_CUSTOMER_ID=CUST-1004
EMBED_TOKEN— Node BFF reads this and sends it in the session mint body.VITE_EMBED_TOKEN— the page reads this and sets theembed-tokenattribute.
VITE_EMBED_API_KEY — the API key stays server-only.
ec_live_… in HTML
Only the embed token UUID and tenant id belong in the browser.
The custom element uses session-url so the BFF holds the API key.
4. Add the bff script to package.json
Open package.json. Inside the existing "scripts" object, add the bff line
(keep dev / build / preview). Example:
File: package.json
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"bff": "node --env-file=.env server.mjs"
}
}
Save. From now on npm run bff starts server.mjs with your .env.
5. Create vite.config.js (proxy /api/…)
Vite’s vanilla template often ships without a config file — that is normal.
Create vite.config.js in the project root (next to package.json) so
session-url="/api/analytics/session" on http://localhost:5173 is forwarded to the BFF on port 3090.
- Create the file:
New-Item vite.config.js(PowerShell) or New File in your editor. - Paste the entire contents below and save:
import { defineConfig } from "vite";
export default defineConfig({
server: {
proxy: {
"/api/analytics/session": {
target: "http://localhost:3090",
changeOrigin: true,
},
},
},
});
If a vite.config.ts already exists instead, add the same server.proxy block there
(do not create a second config). After saving, restart npm run dev.
6. Replace index.html + src/main.js
- Replace all of
index.htmlwith:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Embedded Canvas — HTML</title>
<style>
body { margin: 0; font-family: system-ui, sans-serif; color: #0f172a; }
.bar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; padding: 12px; }
.bar input { padding: 6px 8px; min-width: 140px; }
.meta { padding: 0 12px; color: #64748b; font-size: 14px; }
#mount { min-height: 70vh; }
</style>
</head>
<body>
<p class="meta">Tenant: <code id="tenantLabel"></code></p>
<div class="bar">
<input id="city" placeholder="city" />
<input id="serviceLine" placeholder="service_line" />
<button type="button" id="apply">Apply</button>
</div>
<div id="mount">
<embedded-canvas-analytics
id="embed"
session-url="/api/analytics/session"
layout="chromeless"
></embedded-canvas-analytics>
</div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
- Replace all of
src/main.jswith:
import { registerEmbeddedCanvasAnalyticsElement } from "@embeddedcanvas/embed-sdk/web-component";
registerEmbeddedCanvasAnalyticsElement();
const EMBED_TOKEN = import.meta.env.VITE_EMBED_TOKEN || "YOUR_EMBED_TOKEN_UUID";
const tenantId = import.meta.env.VITE_TEST_CUSTOMER_ID || "CUST-1004";
const embed = document.getElementById("embed");
embed.setAttribute("embed-token", EMBED_TOKEN);
embed.setAttribute("customer-id", tenantId);
document.getElementById("tenantLabel").textContent = tenantId;
document.getElementById("apply").onclick = async () => {
const city = document.getElementById("city").value.trim();
const serviceLine = document.getElementById("serviceLine").value.trim();
const extra = {};
if (city) extra.city = city;
if (serviceLine) extra.service_line = serviceLine;
// setFilters remints via session-url POST with filters.extra (SDK 1.8.3+)
await embed.setFilters(Object.keys(extra).length ? { extra } : {});
};
-
Confirm
VITE_EMBED_TOKENandVITE_TEST_CUSTOMER_IDin.envmatch the BFF values. After editing.env, restartnpm run dev. -
You can delete unused starter files (
src/counter.js, default CSS) if the template left them — keep the mount working first.
session-url— browserPOSTs{ customerId, filters? }to your BFF (never calls Embedded Canvas with the API key).embed-token/customer-id— Step 3 UUID + RLS tenant (safe in the browser).setFilters({ extra })— remints the session with host-owned filter values (requires@embeddedcanvas/embed-sdk@^1.8.3).
7. Run BFF + Vite and open the app
- Terminal A:
npm run bff→ expectBFF listening on http://localhost:3090. - Terminal B:
npm run dev→ open the URL Vite prints (usuallyhttp://localhost:5173). - You should see the dashboard for
VITE_TEST_CUSTOMER_ID.
POST /api/analytics/session fails (proxy has nowhere to go).
If only the BFF is running, there is no page to open.
Quick troubleshooting
Configuration incompleteon the element → missingembed-token/customer-id(checkVITE_*and restart Vite).404/ HTML instead of JSON on session → proxy missing or Vite not restarted aftervite.config.jschange.401/403from session → wrongEMBED_API_KEY, embed token, or tenant; confirm Part I allowlists includehttp://localhost:5173.- Blank iframe → dual allowlist (runtime Allowed Domains + control-plane iframe parent origins); empty Allowed Domains = allow any origin.
setFilters is not a function→ upgrade to@embeddedcanvas/embed-sdk@^1.8.3.
Suggested layout when finished:
my-analytics-html/
.env ← API key + tokens (gitignored)
server.mjs ← BFF
package.json ← "bff" script
vite.config.js ← proxy /api/analytics/session → :3090
index.html ← host filter UI + <embedded-canvas-analytics>
src/main.js ← register element, set attributes, Apply → setFilters
Host-owned filters (non-native UI) — step by step
White-label filter UX
Native filters are the filter controls inside the analytics builder / iframe.
Host-owned filters are inputs in your app (the city / service_line boxes in Part II).
Your app collects values → sends them as filters.extra on session mint → Embedded Canvas
applies them and syncs matching native filters. End users never need the native filter bar.
F-1. Create matching native filters in the builder (required)
- Open the dashboard in the analytics builder (Part I).
- Add a native filter for each column your host UI will drive
(examples used in these tutorials:
city,service_line). -
The filter must target the same dataset column you will send in
filters.extra. Keys are matched by column name (case/spelling must match warehouse values too). - Publish / save the dashboard.
-
Optional white-label: hide the native filter bar in the builder embed settings / keep
layoutMode: "chromeless"(Part II samples already do this).
filters.extra key, or the typed value does not exist in the data (wrong spelling/case).
F-2. Collect values in your app UI
Part II already adds simple inputs (e.g. city, service_line) and an Apply button. Those are host-owned filters — not Superset UI.
F-3. Send values on session mint as filters.extra
Your session-route BFF (server.mjs) must include non-empty host values under filters.extra
when calling POST /public/embed/v1/sessions (directly or via your BFF):
{
"embedToken": "…",
"customerId": "co_alltrade",
"filters": {
"extra": {
"city": "Denver",
"service_line": "Electrical"
}
}
}
In Part II sample apps, the browser posts that shape to /api/analytics/session;
the BFF forwards filters to Embedded Canvas.
Empty strings are omitted — only filled inputs are sent.
F-4. On Apply, remint (do not only update local React/Vue state)
In the HTML sample, Apply builds extra from the city / service_line inputs, then calls await embed.setFilters({ extra }) on the custom element. The element remints through your BFF with filters.extra (SDK 1.8.3+). Changing the inputs alone does not remint — you must click Apply. Native filters for those columns must exist on the dashboard (F-1).
Changing an input alone does nothing until Apply remints the guest session with the new
filters.extra. Charts then reload under RLS + the new filter values.
F-5. Verify
- DevTools → Network →
POST …/session(or your BFF path) → confirm JSON includesfilters.extra. - Type a value you know exists for the current tenant → Apply → chart numbers should change.
- Clear inputs → Apply → broader (or unfiltered) result for that tenant.
filters.extra at mint time.
Both are required for white-label filter UX on the SDK + session route path.