Skip to content

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.

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.

Mint a token
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

Terminal window
curl 'https://YOUR-WORKSPACE.saiku.bi/rest/saiku/api/embed/tokens' \
-u admin:admin

Admins can list every token in the system with ?all=true.

Revoke a token

Terminal window
curl -X DELETE 'https://YOUR-WORKSPACE.saiku.bi/rest/saiku/api/embed/tokens/<token>' \
-u admin:admin

Revocation 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:

Terminal window
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

Terminal window
# List your public grants
curl 'https://YOUR-WORKSPACE.saiku.bi/rest/saiku/api/embed/public' \
-u admin:admin
# Revoke
curl -X DELETE 'https://YOUR-WORKSPACE.saiku.bi/rest/saiku/api/embed/public?kind=query&path=shared/public-chart.saiku' \
-u admin:admin

Attribute reference

AttributeDefaultNotes
server(optional)Origin of your Saiku launcher, e.g. https://YOUR-WORKSPACE.saiku.bi. Leave empty for same-origin
pathrequiredkind="query": query path (.saiku) · kind="dashboard": dashboard path (.saikudash) · kind="ai": cube ref connection/catalog/schema/cubeName
kindqueryquery, dashboard, or ai
token(none)Embed token from POST /saiku/api/embed/tokens. Omit for anonymous public reads
rendertableFor kind="query": table, matrix, chart, or kpi
modebarFor render="chart": bar, line, or pie
height400pxCSS 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
themelightlight, 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:

EventdetailFires 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 series
  • line — same layout, line series
  • pie — 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 typeWhat renders
textThe tile’s text, as a paragraph
chartAn ECharts chart (uses tile.chartType as the mode)
kpiA single big number from the tile’s measure
filterSkipped — 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:

Terminal window
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_INVALID 401 — 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 Referer header 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.

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 HTML
  • Referrer-Policy: no-referrer — never leak the token via Referer on outbound assets
  • Cache-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 marked to 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