E EMBAN / Docs

How Emban Works

Emban lets you build one analytics dashboard and show it to every customer — each seeing only their own data. No per-viewer fees, no hosted BI platform, no Metabase. Just a self-hosted Go binary talking to your ClickHouse.

The core idea: You design a single published dashboard. Each customer gets a signed embed session scoped to their tenant_id. The same dashboard shows different data per customer — enforced server-side, not by filters the user can remove.

Architecture at a glance

Data flow

Events flow from your backend into ClickHouse. Your customers see their slice through signed embed sessions.

Your Backend                  Emban                      Your Customer's Browser
───────────                   ─────                      ──────────────────────

POST /v1/events ──────────►  ClickHouse (analytics)
  tenant_id: "acme"                │
  event_name: "api.call"           │
  numeric_props: {tokens: 150}     │
                                   ▼
POST /v1/embed-sessions ──►  Signed JWT token ──────────► <iframe src="embed_url">
  tenant_id: "acme"            │                            │
  dashboard_id: "dash_..."     │                            ▼
  expires_in: 3600             │                         Dashboard renders
                               │                         (only acme's data)
                               │
                        PostgreSQL (config)
                        dashboards, users, orgs

The full flow, step by step

1. You sign up and get an API key

Register at /app/register. An organization and a default admin API key are created automatically. You can also create scoped keys later:

ScopeCan doUse for
adminEverything: dashboards, embeds, ingestion, settingsYour trusted backend
ingestOnly POST /v1/eventsData pipeline, edge workers
readRead-only dashboard/query access via user sessionsViewer/editor admin-console sessions

2. You send events from your backend

Events are the raw data your dashboards visualize. Each event has a tenant_id (your customer), an event_name, and optional properties:

POST /v1/events
Authorization: Bearer YOUR_INGEST_KEY

{
  "events": [{
    "tenant_id": "acme",
    "event_name": "api.call",
    "timestamp": "2026-04-14T10:00:00Z",
    "user_id": "user_42",
    "string_props": {
      "endpoint": "/chat",
      "model": "gpt-4",
      "status": "success"
    },
    "numeric_props": {
      "tokens": 150,
      "latency_ms": 230
    }
  }]
}

Events go straight to ClickHouse. You can send up to 1,000 events per batch. The tenant_id is what isolates data between your customers.

What are string_props vs numeric_props?
string_props are dimensions you filter or group by (model, endpoint, region).
numeric_props are values you aggregate: sum, average, p95, p99 (tokens, latency, cost).

3. You build a dashboard in the visual editor

Open the Builder and create a dashboard. Or let Emban auto-detect your event schema and generate one:

POST /v1/discover/auto-create
Authorization: Bearer YOUR_ADMIN_KEY

The builder lets you:

Dashboards start as drafts. Editing a draft never affects what your customers see.

4. You publish the dashboard

Publishing creates an immutable snapshot of the dashboard configuration. This is what your customers see in embeds.

POST /v1/dashboards/YOUR_DASHBOARD_ID/publish
Authorization: Bearer YOUR_ADMIN_KEY

You can keep editing the draft after publishing. When you're ready, publish again to update. You can also restore the draft to the last published version if you want to undo changes.

State 1 Draft Editable in the builder. Not visible to customers. Preview with any tenant_id.
State 2 Published Frozen snapshot served to embeds. Draft keeps evolving independently.
Action Unpublish Reverts to draft. Embeds stop working until you publish again.

5. Your backend creates a signed embed session

This is the key security boundary. When a customer loads their dashboard page in your app, your backend creates a short-lived, tenant-scoped session:

POST /v1/embed-sessions
Authorization: Bearer YOUR_ADMIN_KEY

{
  "tenant_id": "acme",
  "dashboard_id": "YOUR_DASHBOARD_ID",
  "expires_in": 3600
}

Response:

{
  "token": "eyJhbGc...",
  "embed_url": "https://emban.sidelabs.dev/embed/dash_...?token=eyJhbGc...",
  "expires_at": "2026-04-14T14:00:00Z"
}

The embed_url is what you pass to the frontend. It's a signed JWT that encodes:

Never expose your admin API key to the browser. The embed session flow is designed so your backend creates the token (using the admin key), then passes only the embed_url to the frontend. The customer never sees your API key.

6. Your frontend embeds the dashboard

Three options, from simplest to most control:

Option A: Plain iframe

<iframe
  src="EMBED_URL"
  width="100%"
  height="600"
  style="border: none; border-radius: 8px"
></iframe>

Option B: Browser helper — adds auto-resize, event listeners, and runtime control

<div id="analytics"></div>
<script src="https://emban.sidelabs.dev/sdk/emban-embed.js"></script>
<script>
  const dash = Emban.create({
    container: '#analytics',
    embedUrl: 'EMBED_URL'
  });

  // React to customer interactions
  dash.on('ready', () => console.log('Dashboard loaded'));
  dash.on('drill', (e) => console.log('Clicked:', e.dimension, e.value));
  dash.on('filter', (e) => console.log('Filtered:', e.filters));

  // Control the dashboard from your app
  dash.setFilters({ period: '7d', dimensions: { model: 'gpt-4' } });
  dash.setTheme({ primaryColor: '#0f9d58' });
</script>

Option C: React wrapper

import { EmbanEmbedFrame } from '@emban/embed-helper/react';

function CustomerDashboard({ embedUrl }) {
  return (
    <EmbanEmbedFrame
      embedUrl={embedUrl}
      onReady={() => console.log('Loaded')}
      onDrill={(e) => console.log(e.dimension, e.value)}
      containerStyle={{ minHeight: 600, borderRadius: 8 }}
    />
  );
}

Option D: Native React components (no iframe) — for full control over layout and styling

import { EmbanProvider, EmbanDashboard } from '@emban/react';

function CustomerDashboard({ token, dashboardId }) {
  return (
    <EmbanProvider
      host="https://emban.sidelabs.dev"
      token={token}
      dashboardId={dashboardId}
    >
      <EmbanDashboard />
    </EmbanProvider>
  );
}

Controlling what customers see

When creating an embed session, you can optionally restrict what the customer can do:

POST /v1/embed-sessions

{
  "tenant_id": "acme",
  "dashboard_id": "dash_...",
  "expires_in": 3600,
  "permissions": {
    "locked_filters": {
      "model": "gpt-4"
    },
    "hidden_widgets": ["w_internal_costs"],
    "allowed_periods": ["24h", "7d", "30d"],
    "allow_drill_down": true,
    "max_date_range_days": 90
  }
}
PermissionWhat it does
locked_filtersPre-set filter values the customer cannot change. Useful for restricting to a specific plan, region, or product tier.
hidden_widgetsWidget IDs that won't render for this customer. Use for internal-only metrics.
allowed_periodsWhich time periods the customer can select. Others are hidden.
allow_drill_downWhether clicking a chart data point opens a detail view.
max_date_range_daysMaximum lookback window. Prevents expensive queries on large date ranges.

Browser runtime: commands and events

Once the dashboard is mounted, the host page and embed communicate via postMessage. The browser helper wraps this into a clean API:

DirectionEvent / CommandWhen
embed → hostreadyDashboard finished loading
embed → hostresizeContent height changed (for auto-sizing)
embed → hostdrillCustomer clicked a chart data point
embed → hostfilterCustomer changed a filter or period
embed → hosterrorSomething went wrong
host → embedsetFiltersProgrammatically set filters from your app
host → embedsetThemeChange colors/fonts at runtime
host → embedreloadRefresh data without reloading the page

Typical integration (Node.js / Next.js)

Backend route — creates a session when your customer visits their dashboard page:

// app/api/analytics/session/route.ts  (Next.js)
import Emban from '@emban/sdk';

const emban = new Emban({
  host: 'https://emban.sidelabs.dev',
  apiKey: process.env.EMBAN_API_KEY  // admin scope
});

export async function GET(request) {
  const tenantId = getTenantFromSession(request);  // your auth logic

  const session = await emban.createEmbedSession({
    tenantId,
    dashboardId: process.env.EMBAN_DASHBOARD_ID,
    expiresIn: 3600
  });

  return Response.json({ embedUrl: session.embedUrl });
}

Frontend component — fetches the session and renders the dashboard:

// components/CustomerDashboard.tsx
'use client';
import { EmbanEmbedFrame } from '@emban/embed-helper/react';
import { useEffect, useState } from 'react';

export function CustomerDashboard() {
  const [embedUrl, setEmbedUrl] = useState('');

  useEffect(() => {
    fetch('/api/analytics/session')
      .then(r => r.json())
      .then(d => setEmbedUrl(d.embedUrl));
  }, []);

  if (!embedUrl) return <div>Loading analytics...</div>;

  return (
    <EmbanEmbedFrame
      embedUrl={embedUrl}
      containerStyle={{ minHeight: 600 }}
    />
  );
}

Widget types

KindChart optionsBest for
kpistat, progressSingle number with trend: total calls, avg latency, error rate
timeseriesarea, line, barMetric over time: daily API calls, weekly tokens consumed
breakdownbar, listTop N by dimension: calls per model, errors per endpoint
tabletableTabular data with sorting

Each widget queries one event type with one metric:

MetricWhat it computesRequires numeric_prop?
countTotal eventsNo
count_uniqueDistinct values of a propertyNo (uses user_id by default)
sumSum of a numeric propertyYes
avgAverage of a numeric propertyYes
p9595th percentileYes
p9999th percentileYes

Alerts

Set up alerts to get notified when metrics cross a threshold. Alerts evaluate every 60 seconds.

POST /v1/alerts
Authorization: Bearer YOUR_ADMIN_KEY

{
  "name": "High error rate",
  "dashboard_id": "dash_...",
  "widget_id": "w_errors",
  "condition_type": "threshold",
  "operator": "gt",
  "threshold_value": 100,
  "webhook_url": "https://your-app.com/webhooks/emban",
  "email_to": ["ops@your-company.com"],
  "enabled": true
}

Alerts can deliver via:

Team management

Invite team members with different roles:

RoleCan do
ownerEverything + delete org + change roles
adminCreate/edit/publish dashboards, manage API keys, create embeds, manage alerts
memberView dashboards and usage (read-only)
POST /v1/members/invite
Authorization: Bearer YOUR_ADMIN_KEY

{
  "email": "teammate@your-company.com",
  "role": "admin"
}

Key concepts glossary

TermMeaning
TenantYour customer. Identified by tenant_id in events and embed sessions. One dashboard, many tenants.
EventA data point your backend sends: an API call, a page view, a transaction. Has string and numeric properties.
DashboardA collection of widgets with filters and a theme. Has draft and published states.
WidgetA single chart or KPI card on a dashboard. Each widget has its own query configuration.
Embed sessionA short-lived signed JWT that grants a specific tenant access to a specific published dashboard.
Published configAn immutable snapshot of the dashboard. What embeds actually render. Draft changes don't affect it.
ScopeCredential permission level. API keys are admin or ingest; user sessions can also resolve to read-only access for viewer/editor roles.
DimensionA string property you can filter or group by (model, endpoint, status, region).
MetricThe aggregation applied to events: count, sum, avg, p95, p99, count_unique.

What's next