Embedding Saiku
The <saiku-embed/> Web Component lets you embed a saved Saiku query or
dashboard inside any HTML page — your marketing site, a blog post, a
customer portal, a wiki, or a React / Vue dashboard. Same custom element
works in every host: React JSX, Vue templates, Svelte, plain HTML.
It’s a real Web Component, not a framework wrapper, so the host page doesn’t need to know anything about Saiku internals.
Install
Two equivalent ways to load the bundle. Pick whichever fits your host page.
No build step — point a <script> tag at the bundle your Saiku
already serves:
<script src="https://YOUR-WORKSPACE.saiku.bi/ui/saiku-embed.js"></script>The download is around 213 KB gzipped. Browsers cache it so the second page on the same origin gets it instantly.
For React / Vue / SPA projects that bundle their own JS:
npm install @concepttocloud/saiku-embedimport "@concepttocloud/saiku-embed";The import has the side effect of registering the saiku-embed tag
globally — no further setup. The npm release tracks the Saiku
release one-to-one.
React teams get a typed wrapper package with autocompleted props:
npm install @concepttocloud/saiku-embed-react reactimport { SaikuEmbed } from "@concepttocloud/saiku-embed-react";
<SaikuEmbed server="https://YOUR-WORKSPACE.saiku.bi" token={token} path="homes/admin/Sales.saiku" render="chart" mode="bar"/>Full reference on the React SDK page.
A worked example
The minimum viable embed — a saved query, rendered as a table:
<saiku-embed server="https://YOUR-WORKSPACE.saiku.bi" token="tx-..." path="homes/admin/Examples/Trend.saiku" height="400px"></saiku-embed>The three load-bearing attributes are server (origin of your Saiku),
path (repository path of the saved query), and token (the embed
token — see Minting a token below). Everything
else has sensible defaults.
Minting a token
Tokens are minted by an authenticated user who can read the resource. The token is opaque and scoped: it grants read-only access to one saved query or dashboard, expires after the TTL you specify, and can be revoked at any time.
curl -X POST 'https://YOUR-WORKSPACE.saiku.bi/rest/saiku/api/embed/tokens' \ -u admin:admin \ -H 'Content-Type: application/json' \ -d '{ "resourceKind": "query", "resourcePath": "homes/admin/Examples/Trend.saiku", "ttlHours": 72, "label": "Marketing landing page" }'
# Response:# {# "status": "OK",# "token": "tx-...",# "resourceKind": "query",# "resourcePath": "homes/admin/Examples/Trend.saiku",# "expiresAt": 1739102400000# }Paste the token into your host page’s <saiku-embed token="...">.
TTL bounds. Default is 72 hours. Maximum is 30 days (720 hours). A short TTL is the safest default for a token that travels through arbitrary host pages.
Per-user limit. Each user may hold up to 200 active tokens at once. Revoke unused tokens to free the budget.
List your tokens
curl 'https://YOUR-WORKSPACE.saiku.bi/rest/saiku/api/embed/tokens' \ -u admin:adminAdmins can list every token in the system with ?all=true.
Revoke a token
curl -X DELETE 'https://YOUR-WORKSPACE.saiku.bi/rest/saiku/api/embed/tokens/<token>' \ -u admin:adminRevocation takes effect on the very next request — there’s no guest session to hijack.
Public (anonymous) embeds
If a resource should be readable without a token — a public chart on your landing page, an open dashboard on a blog — flip it publicly embeddable:
curl -X POST 'https://YOUR-WORKSPACE.saiku.bi/rest/saiku/api/embed/public' \ -u admin:admin \ -H 'Content-Type: application/json' \ -d '{ "resourceKind": "query", "resourcePath": "shared/public-chart.saiku", "label": "Homepage chart" }'Then the host page omits the token entirely:
<saiku-embed server="https://YOUR-WORKSPACE.saiku.bi" path="shared/public-chart.saiku" render="chart"></saiku-embed>List + revoke public grants
# List your public grantscurl 'https://YOUR-WORKSPACE.saiku.bi/rest/saiku/api/embed/public' \ -u admin:admin
# Revokecurl -X DELETE 'https://YOUR-WORKSPACE.saiku.bi/rest/saiku/api/embed/public?kind=query&path=shared/public-chart.saiku' \ -u admin:adminAttribute reference
| Attribute | Default | Notes |
|---|---|---|
server | (optional) | Origin of your Saiku launcher, e.g. https://YOUR-WORKSPACE.saiku.bi. Leave empty for same-origin |
path | required | kind="query": query path (.saiku) · kind="dashboard": dashboard path (.saikudash) · kind="ai": cube ref connection/catalog/schema/cubeName |
kind | query | query, dashboard, or ai |
token | (none) | Embed token from POST /saiku/api/embed/tokens. Omit for anonymous public reads |
render | table | For kind="query": table, matrix, chart, or kpi |
mode | bar | For render="chart": bar, line, or pie |
height | 400px | CSS height (a min-height on the rendered surface) |
space | (none) | For kind="ai": Agent Space persona id — scopes the assistant server-side |
filter | (none) | For kind="query": JSON array of slicer overrides applied at embed time |
theme | light | light, dark, or auto (follow the host’s prefers-color-scheme) |
Attributes are reactive — the component re-renders whenever any of them change. In React, Vue, or Svelte, binding state to these props just works.
Events
The element emits namespaced CustomEvents so the host page can react
to what happens inside the embed. All bubble and are composed, so a
listener on the <saiku-embed> element receives them:
| Event | detail | Fires when |
|---|---|---|
saiku:load | { kind, rows } | a query / matrix / kpi surface loads |
saiku:error | { message } | a query load fails (friendly message) |
saiku:select | { row } | a table row is clicked (render="table") |
saiku:ai-query | { question, degraded } | an AI ask resolves (kind="ai") |
const el = document.querySelector("saiku-embed");el.addEventListener("saiku:load", (e) => console.log("loaded", e.detail.rows, "rows"));el.addEventListener("saiku:select", (e) => showDetail(e.detail.row));In React (via @concepttocloud/saiku-embed-react) the same events are
exposed as onLoad / onError / onSelect / onAiQuery callback props.
Examples
Saved query as a table
<saiku-embed server="https://YOUR-WORKSPACE.saiku.bi" token="tx-..." path="homes/admin/Examples/Trend.saiku"></saiku-embed>Saved query as a chart
<saiku-embed server="https://YOUR-WORKSPACE.saiku.bi" token="tx-..." path="homes/admin/Examples/Sales.saiku" render="chart" mode="bar" height="500px"></saiku-embed>Chart modes:
bar(default) — categorical bars; first non-numeric column is the category, every numeric column becomes its own seriesline— same layout, line seriespie— first numeric column only; falls back to “No numeric series” if the result is text-only
Tooltips show the server-formatted cell strings ($1,234,
12.3%, etc.) so the host page sees the same captions the Saiku
workbench would.
Saved dashboard
<saiku-embed server="https://YOUR-WORKSPACE.saiku.bi" token="tx-..." kind="dashboard" path="homes/admin/exec.saikudash" height="700px"></saiku-embed>The component fetches the dashboard layout once and then runs one tile query per chart / KPI tile in parallel under the dashboard owner’s data scope. Tile types in v1:
| Tile type | What renders |
|---|---|
text | The tile’s text, as a paragraph |
chart | An ECharts chart (uses tile.chartType as the mode) |
kpi | A single big number from the tile’s measure |
filter | Skipped — filter widgets are workbench-only in v1 |
| other | ”Unsupported tile” placeholder |
Anonymous public embed
When the resource has a public grant, omit the token:
<saiku-embed server="https://YOUR-WORKSPACE.saiku.bi" path="shared/public.saiku" render="chart" mode="pie"></saiku-embed>AI ask widget over a cube
kind="ai" renders a chat surface that answers natural-language
questions about a single cube. path is a cube ref, and the token is
minted with resourceKind: "ai" (see below). Add a space to scope it
to an Agent Space persona — the persona’s system prompt
and cube allowlist are enforced server-side, so the widget can only
answer within its brief.
<saiku-embed server="https://YOUR-WORKSPACE.saiku.bi" token="tx-..." kind="ai" path="foodmart/FoodMart/FoodMart/Sales" space="foodmart-sales-analyst" height="600px"></saiku-embed>Mint the token against a cube ref rather than a file path:
curl -X POST 'https://YOUR-WORKSPACE.saiku.bi/rest/saiku/api/embed/tokens' \ -u admin:admin \ -H 'Content-Type: application/json' \ -d '{ "resourceKind": "ai", "resourcePath": "foodmart/FoodMart/FoodMart/Sales", "ttlHours": 72, "label": "Assistant on marketing site" }'Inside React
import "@concepttocloud/saiku-embed";
export function SalesEmbed({ token }: { token: string }) { return ( <saiku-embed server="https://YOUR-WORKSPACE.saiku.bi" token={token} path="homes/admin/Examples/Sales.saiku" render="chart" mode="line" height="500px" /> );}Inside Vue
<template> <saiku-embed server="https://YOUR-WORKSPACE.saiku.bi" :token="token" path="homes/admin/Examples/Sales.saiku" render="chart" /></template>
<script setup lang="ts">import "@concepttocloud/saiku-embed";defineProps<{ token: string }>();</script>Theming
The embed renders inside an
open shadow root,
so host page CSS can’t leak in and embed CSS can’t leak out. To
recolour, set CSS variables on the host page’s saiku-embed selector:
saiku-embed { --saiku-embed-fg: #0f172a; --saiku-embed-bg: transparent; --saiku-embed-border: #cbd5e1; --saiku-embed-header-bg: #f1f5f9; --saiku-embed-tile-bg: #ffffff; --saiku-embed-row-hover: #e2e8f0; --saiku-embed-negative: #b91c1c; --saiku-embed-error: #b91c1c; --saiku-embed-muted: #64748b;}Dark mode is just a media query that flips the same variables:
@media (prefers-color-scheme: dark) { saiku-embed { --saiku-embed-fg: #f1f5f9; --saiku-embed-bg: #0f172a; --saiku-embed-border: #334155; --saiku-embed-header-bg: #1e293b; --saiku-embed-tile-bg: #1e293b; --saiku-embed-row-hover: #334155; }}Security model
The embed surface is designed for hostile host pages — your saved query might end up on a third-party site that you don’t control. The defaults reflect that.
Tokens are opaque, server-authoritative, revocable
- Tokens are 256-bit random ids, Base64-URL encoded — unguessable.
- They carry no embedded claims: the server holds the authoritative record (resource kind + path, owner snapshot, expiry, revoked flag) and looks it up on every request. Revocation takes effect on the very next request — no JWT-style “wait for the signature to expire.”
- Each token pins exactly one resource. Replaying a token against
any other resource (wrong path, wrong kind, expired, revoked) returns
the same opaque
EMBED_INVALID401 — probes can’t enumerate.
Header-only transport
The token travels as the X-Saiku-Embed-Token HTTP header. We
deliberately don’t accept ?token=… query parameters because URL
params leak into:
- servlet access logs
- fronting proxy logs
- browser history
- the
Refererheader sent on every outbound asset on the host page
The bundled JS reads the token from your <saiku-embed token="…">
attribute and sends it as a header on every fetch.
Cross-origin cookie isolation
The embed fetches with credentials: "omit". If the host page’s user
happens to be logged into Saiku in another tab, that session cookie
does not flow with embed reads. The token IS the only auth carrier
on this surface.
Owner data scope
Both tokens and public grants snapshot the owner’s role list at
mint / grant time and run the embed query under that identity via
sessionService.runAs. A publicly-embedded query that uses
session-injected filters renders against the grantor’s perspective,
not the (empty) anonymous default — what you authorised at grant time
is exactly what the public sees.
Defence-in-depth response headers
Every embed reply carries:
X-Content-Type-Options: nosniff— browsers must not MIME-sniff a JSON body (which might carry text-tile HTML or member captions) and execute it as HTMLReferrer-Policy: no-referrer— never leak the token viaRefereron outbound assetsCache-Control: no-store, max-age=0— embedded business data is never cached by proxies or browser history
Friendly errors
When a fetch fails (expired token, revoked token, wrong path, missing
public grant), the host page sees a generic “This embed is
unavailable.” — not the raw EMBED_INVALID body. The host page is a
third party and doesn’t need to know whether the failure was an
expired token or a revoke.
Bundle size
Around 213 KB gzipped at the time of writing — Svelte 5 custom element runtime + ECharts (core + bar / line / pie + four common components, modular tree-shaken) + the embed renderers. The bundle is cached aggressively by the browser; the second embed on the same origin gets it instantly.
Limitations
- Filter tiles are skipped on dashboards. The embed renders the authored data as-is without an interactive filter bar.
- Markdown in text tiles renders as plain text. We don’t bundle
markedto keep the size down.
See also
- React SDK — typed React wrapper around
<saiku-embed>for React shops - Authentication — minting API keys for programmatic Saiku Cloud access (different from embed tokens)
- MCP — connecting LLMs to your cubes