E EMBAN / Docs

Vanilla JavaScript

This guide embeds an Emban dashboard into any HTML page using nothing but the browser's native APIs — no framework, no build step, no npm install. The same pattern works inside Vue, Svelte, Angular, plain server-rendered HTML, or a static site.

Mint sessions server-side. Embed sessions are short-lived JWTs signed with your admin API key. Never ship the admin key to the browser. Mint the session on your backend (any of the Node, curl, or language of your choice) and pass the resulting embed_url to the page.

The minimal embed

An Emban dashboard is a standalone HTML page that runs inside an iframe you control. The simplest integration is two lines — render the iframe with the embed_url:

<!-- index.html -->
<iframe
  src="https://emban.sidelabs.dev/embed/dash_abc123?token=eyJhbGc..."
  style="width:100%;height:640px;border:0;"
  title="Usage dashboard"></iframe>

That's enough for a working dashboard. The remainder of this guide is about the optional upgrades: auto-resize, cross-frame theming, external filter control, and reacting to user clicks inside the dashboard.

Fetch the embed URL from your backend

Most integrations keep session minting on a trusted endpoint of your own (e.g. /api/emban-session). The page fetches it at load time and injects the iframe:

<div id="emban-mount" style="min-height:640px"></div>
<script>
async function mountEmban() {
  // Your backend returns { embed_url, expires_at }
  const r = await fetch('/api/emban-session', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      dashboard_id: 'dash_abc123',
      tenant_id: 'acme',
    }),
  });
  const { embed_url } = await r.json();

  const iframe = document.createElement('iframe');
  iframe.src = embed_url;
  iframe.style.cssText = 'width:100%;height:640px;border:0;';
  iframe.title = 'Emban dashboard';
  document.getElementById('emban-mount').replaceChildren(iframe);
}

mountEmban();
</script>

See Node.js Backend for the server side of /api/emban-session — it's a thin wrapper around POST /v1/embed-sessions.

Auto-resize the iframe

Emban dashboards post their rendered height to the parent window whenever content changes. Listening for that message lets the iframe track the dashboard's real height instead of hardcoding a pixel value:

window.addEventListener('message', (event) => {
  // Verify origin — only trust messages from the Emban domain
  if (event.origin !== 'https://emban.sidelabs.dev') return;

  let msg;
  try {
    msg = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
  } catch {
    return;
  }
  if (!msg || !msg._emban) return;

  if (msg.type === 'resize' && typeof msg.height === 'number') {
    const iframe = document.querySelector('iframe[src^="https://emban.sidelabs.dev/embed/"]');
    if (iframe) iframe.style.height = msg.height + 'px';
  }
});
Always check event.origin. Any page on the web can postMessage into your window. Without an origin check, an attacker could inject fake resize or filter events. Pin it to your Emban domain.

The postMessage bridge

The dashboard sends and receives structured messages. Every Emban message carries _emban: true so you can filter out unrelated traffic, and every message body is either a plain object or a JSON string (older browsers stringify; newer ones pass objects).

Messages the dashboard sends to your page

TypePayloadWhen
ready{dashboardId}The dashboard has mounted and is ready to receive commands.
resize{height}Content height has changed. Fires on initial render and every 500ms poll.
filter{period, drillFilters, ...}User changed a filter, clicked a bar/slice, or selected a period. Fires after each refetch.
period-change{period, previousPeriod}Fired alongside filter when the period specifically changed (handy when you only care about time range).

Messages your page sends to the dashboard

Use iframe.contentWindow.postMessage(msg, 'https://emban.sidelabs.dev'). Your message must carry _emban: true or the dashboard will ignore it.

TypePayloadEffect
setFilters{filters: {...}}Override filter state. Accepts the same shape as the filter message body.
setTheme{theme: {primaryColor, backgroundColor, cardBackground, cardBorder, textColor, mutedColor}}Repaint the dashboard with new colors without remounting.
reload{}Force a data refetch. Useful after you ingest fresh events and want the customer to see them immediately.

A full example with theming + filter sync

<!-- emban-embed.html -->
<div id="emban-mount" style="min-height:640px"></div>
<button id="theme-toggle">Toggle dark mode</button>
<button id="reload-btn">Reload data</button>

<script>
const EMBAN_ORIGIN = 'https://emban.sidelabs.dev';

async function mount() {
  const { embed_url } = await fetch('/api/emban-session', { method: 'POST' })
    .then((r) => r.json());

  const iframe = document.createElement('iframe');
  iframe.src = embed_url;
  iframe.style.cssText = 'width:100%;height:640px;border:0;';
  iframe.title = 'Emban dashboard';
  document.getElementById('emban-mount').replaceChildren(iframe);

  // Helpers to talk to the dashboard
  const post = (msg) => {
    if (!iframe.contentWindow) return;
    iframe.contentWindow.postMessage({ _emban: true, ...msg }, EMBAN_ORIGIN);
  };

  // Listen for messages from the dashboard
  window.addEventListener('message', (event) => {
    if (event.origin !== EMBAN_ORIGIN) return;
    let msg;
    try { msg = typeof event.data === 'string' ? JSON.parse(event.data) : event.data; }
    catch { return; }
    if (!msg || !msg._emban) return;

    switch (msg.type) {
      case 'ready':
        console.log('Dashboard mounted:', msg.dashboardId);
        break;
      case 'resize':
        iframe.style.height = msg.height + 'px';
        break;
      case 'filter':
        // User interacted: sync to URL, update other widgets on the page, etc.
        console.log('Filters now:', msg);
        break;
    }
  });

  // Drive the dashboard from host UI
  let dark = false;
  document.getElementById('theme-toggle').addEventListener('click', () => {
    dark = !dark;
    post({
      type: 'setTheme',
      theme: dark
        ? { backgroundColor: '#0c0c0c', textColor: '#f0f0f0', cardBackground: '#181818', cardBorder: '#2a2a2a', mutedColor: '#9a9a9a' }
        : { backgroundColor: '#ffffff', textColor: '#111827', cardBackground: '#f9fafb', cardBorder: '#e5e7eb', mutedColor: '#6b7280' },
    });
  });

  document.getElementById('reload-btn').addEventListener('click', () => {
    post({ type: 'reload' });
  });
}

mount();
</script>

Session expiry and re-mint

Embed tokens expire (default 1 hour, maximum 24). For long-running sessions, re-mint before expiration and swap the src:

async function refreshEmbed(iframe) {
  const { embed_url, expires_in } = await fetch('/api/emban-session', { method: 'POST' })
    .then((r) => r.json());
  iframe.src = embed_url;
  // Re-mint 60 seconds before expiry
  setTimeout(() => refreshEmbed(iframe), (expires_in - 60) * 1000);
}

The simplest approach: mint with a 24-hour TTL and refresh the whole page on expiry. That covers single-session dashboards used in a customer portal. Only worry about mid-session refresh if you know users stay on the page for hours.

Responsive layout

Emban dashboards are responsive down to ~480px wide. Give the iframe a full-width container and Emban handles the rest:

<style>
  .emban-wrap {
    width: 100%;
    max-width: 1200px;  /* pick a ceiling that fits your layout */
    margin: 0 auto;
  }
  .emban-wrap iframe {
    width: 100%;
    border: 0;
    display: block;
  }
</style>

<div class="emban-wrap">
  <iframe id="emban" title="Dashboard"></iframe>
</div>

Combined with the resize listener above, this gives you a dashboard that fills the available width and adjusts its height to its content on every render.

Error handling

Blank iframe Token expired or invalid Mint a fresh session. If the same call keeps returning expired tokens, your server clock may be skewed — Emban allows 60s of clock drift before rejecting.
Empty dashboard No events for this tenant The JWT is valid but the tenant has zero events in the selected period. Send test events first (see the curl guide) or extend the period.
"Powered by Emban" Free plan branding The Free plan injects a small footer. Upgrade to Starter or above to remove it — the flag is viz.remove_branding on the plan spec. See Pricing.
No resize events Origin mismatch Your message listener is probably filtering out messages from Emban because event.origin doesn't match. Double-check you're comparing to the exact Emban deploy URL (including protocol and no trailing slash).

When to reach for a framework

This vanilla approach is enough for most integrations — product analytics dashboards, customer portals, embedded reports. Reach for the framework guides when:

Related: Embed Runtime for the full message contract, Node.js Backend for the server-side session minting code, Tenants for how tenant_id scopes the customer's view, and Permissions for locked filters and hidden widgets on embeds.