Embed analytics in Vue — SDK + session route
ec_live_… off the client, binds customerId to your auth session, and is the
production-safe path for SaaS embeds.
Same pattern as the React tutorial: a Vite Vue app plus a small Node server.mjs BFF in the same folder.
The browser calls your BFF; the BFF mints sessions with your embed API key.
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_EMBED_TOKENin 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_EMBED_TOKEN / createVueEmbedMount / embed.mount({ embedToken }) 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 — Vue + session route (BFF)
Your laptop · follow in order
1. Scaffold the Vite Vue app + install SDKs
In a terminal, run (accept the Vite defaults if prompted):
npm create vite@latest my-analytics-vue -- --template vue
cd my-analytics-vue
npm install
npm install @embeddedcanvas/embed-sdk@^1.7.0 @superset-ui/embedded-sdk
Stay in my-analytics-vue for every remaining step.
Your folder should look roughly like:
my-analytics-vue/
package.json
vite.config.js
index.html
src/
App.vue
main.js
…
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 (my-analytics-vue/, next to package.json), create a new file named exactly server.mjs.
- Create the empty file (pick one):
- macOS / Linux:
touch server.mjs - Windows PowerShell:
New-Item server.mjs - Or use your editor: New File → save as
server.mjsin the project root
- macOS / Linux:
- Paste the entire contents below into
server.mjsand save (do not leave the file empty).
/**
* 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`);
});
Optional reference copy in this repo: examples/integrator/node-bff/server.mjs
(same idea; this tutorial keeps the file inside your Vite app so one project runs both processes).
3. Create project-root .env (paste tokens)
Still in the project root (same folder as server.mjs and package.json), create a file named .env.
Use one .env for both the BFF and Vite.
- Create the file:
touch .env(or New File →.env). - Paste the template below.
- Replace placeholders with Part I values:
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(Vite’s template usually includes it).
# 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 — App.vue reads this (body overrides BFF default)
VITE_TEST_CUSTOMER_ID=CUST-1004
VITE_ and not VUE_?
This app is scaffolded with Vite (npm create vite … --template vue).
Vite only injects env vars that start with VITE_ into import.meta.env.
A name like VUE_TEST_CUSTOMER_ID would not be available in src/App.vue.
(Older Vue CLI projects used VUE_APP_* — that does not apply here.)
EMBED_TOKEN— Node BFF reads this and sends it in the session mint body.VITE_EMBED_TOKEN— Vue reads this viaimport.meta.env.VITE_EMBED_TOKENforembed.mount.
VITE_* vars to the browser, so the client line needs the VITE_ prefix.
Do not create a VITE_EMBED_API_KEY.
App.vue sends VITE_TEST_CUSTOMER_ID as customerId — that value drives RLS,
not the BFF TEST_CUSTOMER_ID default alone.
Builder “Company” filters as admin are not the same as the embed guest claim.
After editing .env, restart npm run bff and npm run dev.
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 the file. From now on npm run bff starts server.mjs with the .env you created.
5. Proxy /api/… in vite.config.js
The Vue app will call fetch("/api/analytics/session") on the Vite origin (http://localhost:5173).
Vite must forward that path to the BFF on port 3090.
Open vite.config.js (or vite.config.ts) and replace it with:
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
"/api/analytics/session": {
target: "http://localhost:3090",
changeOrigin: true,
},
},
},
});
If your file is TypeScript, keep the same server.proxy block.
Do not remove the vue() plugin — the Vite Vue template needs it.
6. Replace src/App.vue
- Open
src/App.vue. - Replace all of its contents with the code below.
-
Do not replace
"YOUR_EMBED_TOKEN_UUID"ifVITE_EMBED_TOKENis set in.env— that string is only a fallback when the env var is missing. -
EMBED_TOKENandVITE_EMBED_TOKENmust be the same Step 3 UUID. - Save. You can ignore unused starter CSS in
main.jsfor now.
<script setup>
import { onBeforeUnmount, onMounted, ref } from "vue";
import { createVueEmbedMount } from "@embeddedcanvas/embed-sdk/vue";
const EMBED_TOKEN = import.meta.env.VITE_EMBED_TOKEN || "YOUR_EMBED_TOKEN_UUID";
// RLS tenant — must match warehouse values for your workspace tenant column
const CUSTOMER_ID = import.meta.env.VITE_TEST_CUSTOMER_ID || "CUST-1004";
const city = ref("");
const serviceLine = ref("");
const mountEl = ref(null);
const filtersRef = { city: "", service_line: "" };
const embed = createVueEmbedMount(() => mountEl.value);
async function mountEmbed() {
await embed.mount({
embedToken: EMBED_TOKEN,
customerId: CUSTOMER_ID,
layoutMode: "chromeless",
fetchSession: () => {
const extra = {};
for (const [k, v] of Object.entries(filtersRef)) {
if (v.trim()) extra[k] = v.trim();
}
return fetch("/api/analytics/session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
customerId: CUSTOMER_ID,
...(Object.keys(extra).length ? { filters: { extra } } : {}),
}),
}).then(async (res) => {
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Session mint failed");
return data;
});
},
});
}
async function applyFilters() {
filtersRef.city = city.value;
filtersRef.service_line = serviceLine.value;
const extra = {};
for (const [k, v] of Object.entries(filtersRef)) {
if (v.trim()) extra[k] = v.trim();
}
await embed.getController()?.setFilters(Object.keys(extra).length ? { extra } : {});
}
onMounted(() => {
void mountEmbed();
});
onBeforeUnmount(() => embed.destroy());
</script>
<template>
<div>
<p style="padding: 12px 12px 0; color: #64748b; font-size: 14px">
Tenant: <code>{{ CUSTOMER_ID }}</code>
</p>
<div style="display: flex; gap: 8px; padding: 12px">
<input v-model="city" placeholder="city" />
<input v-model="serviceLine" placeholder="service_line" />
<button type="button" @click="applyFilters">Apply</button>
</div>
<div ref="mountEl" style="min-height: 70vh" />
</div>
</template>
7. Run BFF + Vite and open the app
You need two terminals, both in my-analytics-vue:
# Terminal 1 — session-route BFF (must stay running)
npm run bff
# Expect: BFF listening on http://localhost:3090
# Terminal 2 — Vite Vue app
npm run dev
# Expect: Local http://localhost:5173
- Open
http://localhost:5173in the browser. - Confirm the page shows your tenant id and the dashboard loads.
-
In DevTools → Network, find
POST /api/analytics/sessionand confirm the JSON body includes"customerId":"…"matchingVITE_TEST_CUSTOMER_ID, and the response is 200. - Optional: type filter values that exist in your data → Apply.
Configure .envfrom the BFF → missing/wrongEMBED_API_KEY,EMBED_TOKEN, or API URL; orserver.mjsleft empty.- Browser network
/api/analytics/sessionfails → BFF not running, or Vite proxy not saved/restarted. EMBED_TOKEN≠VITE_EMBED_TOKEN→ use the same Step 3 UUID on both lines.- Empty charts / RLS → wrong
VITE_TEST_CUSTOMER_ID(builder Company filter ≠ embed claim). - Iframe / allowlist errors → Part I dual allowlist must include
http://localhost:5173. VITE_*empty → restartnpm run devafter editing.env.
Final layout:
my-analytics-vue/
.env ← created in step 3 (secrets + VITE_*)
server.mjs ← created in step 2 (BFF) — must not be empty
package.json ← "bff" script added in step 4
vite.config.js ← proxy added in step 5
src/App.vue ← replaced in step 6
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 Vue sample, Apply copies city / serviceLine into filtersRef, then calls await embed.getController()?.setFilters({ extra }). The SDK remints through your BFF with filters.extra. Updating v-model alone does not remint — you must click Apply.
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.