---
sidebar_position: 5
title: Embedding Dives in your web application
description: Embed interactive MotherDuck Dives in your web app using iframes and embed sessions
feature_stage: preview
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

You can embed Dives in your own web application so your users can interact with live data dashboards without signing in to MotherDuck. Your backend creates an embed session, and your frontend loads the Dive in a sandboxed iframe.

Embedding Dives is available on the **Business plan**.

## Prerequisites

Before you start, you need:

- A **MotherDuck Business plan** account
- A read/write access token for an account with the Admin role. For production, we recommend using a dedicated [service account](/key-tasks/service-accounts-guide/create-and-configure-service-accounts/)
- A Dive you want to embed, with its [data shared](/sql-reference/mcp/share-dive-data) to the target service account that the embedded Dive will run as
- A backend server that can make authenticated API calls

:::tip Use a dedicated service account
We recommend using a service account that does not own databases with the same names as the databases your Dives query. When the service account attaches shared Dive data, the share alias defaults to the source database name. If the service account already has a database with that name, the attach fails. Using a dedicated, empty service account for embedding avoids this conflict.
:::

## How it works

Embedded Dives follow a short server-side flow:

1. **Your backend** calls the MotherDuck API with your access token to create an embed session: an opaque string that contains a read-only session string and the information needed to load the Dive.
2. **Your frontend** renders a sandboxed iframe that loads the Dive from `embed-motherduck.com`, passing the session string.
3. **MotherDuck** loads the Dive and runs live SQL queries.

Your end-users see an interactive dashboard without needing a MotherDuck account.

::::info[Two tokens are in play]
Your service account's access token is a **high-privilege read-write admin token** that stays on your backend and is used only to create embed sessions. The session string it produces contains a **separate, read-only token** that is limited in scope and expires after 24 hours. Only the session string should ever reach the frontend.
::::

```mermaid
sequenceDiagram
    participant M as MotherDuck
    participant B as Your backend
    participant F as Your frontend
    participant E as Embed iframe

    Note over B: Holds your access token
    B->>M: POST /v1/dives/<dive_id>/embed-session
    M-->>B: Session string
    B-->>F: Return session string
    F->>E: Load iframe /sandbox/#session=<session>
    Note over F,E: The session stays in the<br />URL fragment, not the request
    E->>M: Fetch Dive metadata and content
    M-->>E: Return the Dive
```

## Step 1: Create an embed session

Your backend calls the MotherDuck API to create an embed session. The access token used for this call must belong to an account with admin-level access. The session string contains a read-only token that expires after 24 hours.

::::warning[Important]
**Never expose your access token in client-side code.** The access token stays on your backend. Only the session string reaches the browser.
::::


<Tabs groupId="language">
<TabItem value="node" label="Node.js" default>

```javascript
const DIVE_ID = "<your_dive_id>";

const response = await fetch(
  `https://api.motherduck.com/v1/dives/${DIVE_ID}/embed-session`,
  {
    method: "POST",
    headers: {
      // This is the admin account used to generate the embed session.
      Authorization: `Bearer ${MOTHERDUCK_TOKEN}`,
      "Content-Type": "application/json",
    },
    // This is the service account whose compute / perms will be used for the Dive.
    body: JSON.stringify({ username: SERVICE_ACCOUNT_USERNAME }),
  }
);

if (!response.ok) {
  throw new Error(`Failed to create embed session: ${response.status}`);
}

const { session } = await response.json();
// Return this session string to your frontend
```

</TabItem>
<TabItem value="python" label="Python">

```python
import httpx

DIVE_ID = "<your_dive_id>"

response = httpx.post(
    f"https://api.motherduck.com/v1/dives/{DIVE_ID}/embed-session",
    headers={
        "Authorization": f"Bearer {MOTHERDUCK_TOKEN}",
        "Content-Type": "application/json",
    },
    json={"username": SERVICE_ACCOUNT_USERNAME},
)
response.raise_for_status()
session = response.json()["session"]
# Return this session string to your frontend
```

</TabItem>
</Tabs>

Replace `<your_dive_id>` with the ID of your Dive. You can find this in **Settings** > **Dives** or through the [`list_dives`](/sql-reference/mcp/list-dives) MCP tool.

Each session is tied to a single Dive. If you embed multiple Dives on the same page, create a separate embed session for each one. You can use the same service account and access token for all of them. The session string is base64-encoded but **not encrypted** — it contains a read-only (read-scaling) token, the Dive ID, and endpoint URLs. Treat it like a short-lived credential: do not log it or store it in persistent storage.
The embedded Dive runs queries as the service account specified in the session. If you need data isolation (for example, separate databases per region), use separate service accounts scoped to only the data each should access.

## Step 2: Embed the iframe

Add a sandboxed iframe to your page that points to the MotherDuck embed URL. Pass the session string in the URL fragment:

```html
<iframe
  src="https://embed-motherduck.com/sandbox/#session=<session_from_backend>"
  sandbox="allow-scripts allow-same-origin"
  width="100%"
  height="600"
  style="border: none;"
></iframe>
```

Replace `<session_from_backend>` with the session string your backend generated.

The `sandbox` attribute must include `allow-scripts allow-same-origin` for the embed to function.

### Query modes

We recommend getting embedding working with the default **server mode** first, then enabling dual mode afterward. The default server mode is sufficient for most use cases.

By default, embedded Dives run queries server-side through MotherDuck. You can also enable **dual mode**, where queries run on the client (using DuckDB WASM) or the server depending on the query. To use dual mode, add `?queryMode=dual` to the iframe URL:

```html
<iframe
  src="https://embed-motherduck.com/sandbox/?queryMode=dual#session=<session_from_backend>"
  sandbox="allow-scripts allow-same-origin"
  width="100%"
  height="600"
  style="border: none;"
></iframe>
```

#### Server mode data type limitations

Server mode runs queries through the Postgres wire protocol, which does not support all DuckDB data types. Basic types (integers, strings, floats) work fine, but nested types (structs, lists) and some less common timestamp types may not render correctly. If you encounter issues with specific columns, try dual (WASM) mode, which supports the full range of DuckDB types.

### URL structure

| Part | Description |
|------|-------------|
| `embed-motherduck.com/sandbox/` | The MotherDuck embed host |
| `?queryMode=dual` | Optional: enables dual (client + server) query mode |
| `#session=<session>` | The session string, passed in the URL fragment so it is never sent to the server |

The session is placed in the URL fragment (after `#`) rather than the query string. Browsers strip fragments before making HTTP requests, so the session does not appear in server logs or Referer headers.

## Session lifecycle

Embed sessions expire after 24 hours. You have two options for handling expiration:

- **Generate a fresh session per page load.** The simplest approach. Each time a user loads the page, your backend creates a new embed session and passes it to the iframe.
- **Cache and refresh.** Your backend caches the session and refreshes it before it expires. This reduces API calls but adds complexity.

If a session expires while a Dive is open, the embed displays a "Session expired" message. The user needs to reload the page to get a new session.

## Security best practices

- **Keep your access token server-side.** Never include your access token in client-side JavaScript, HTML, or any code that reaches the browser.
- **Use a dedicated service account.** Create a [service account](/key-tasks/service-accounts-guide/create-and-configure-service-accounts/) specifically for embedding, separate from your personal account. The account needs a read/write, Admin-level access token to create embed sessions, but the sessions it generates are always read-only.
- **Sessions are read-only.** The embed session always contains a read-scaling token, so it can only read data, not modify it.
- **Session in URL fragment.** The fragment (`#session=...`) is never sent to the server in HTTP requests, keeping the session out of access logs and referrer headers.
- **Scope service accounts for data isolation.** If you need to restrict which data different users can see (for example, per-region databases), create separate service accounts with access scoped to the appropriate data. The embedded Dive queries data as the service account used to create the session.

## CSP configuration

If your site uses a restrictive [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP), add `embed-motherduck.com` to your `frame-src` directive:

```text
Content-Security-Policy: frame-src https://embed-motherduck.com;
```

Without this, the browser blocks the iframe from loading.

## Troubleshooting

Errors from the embed itself (expired token, Dive not found) appear as messages **inside the iframe**. CSP or network-related errors typically appear only in the **browser developer console**.

| Error message | Cause | Solution |
|---------------|-------|----------|
| "Dive embedding requires a Business plan." | Your organization is not on the Business plan | Upgrade to a [Business plan](https://motherduck.com/pricing/) |
| "Invalid or expired token. Please reload the page." | The session has expired or is malformed | Create a fresh embed session from your backend |
| "Dive not found." | The Dive ID is incorrect or the Dive has been deleted | Verify the Dive ID in **Settings** > **Dives** |
| "Failed to load dive. Please try again." | A generic error occurred while loading | Check your session string and network connectivity, then reload |
| "Can't open share: Share alias cannot be the same as an existing database name. _name_ is already taken and used as a database name." | Your service account already has a database with the same name as one of the Dive's shared databases | Rename or [detach](/key-tasks/database-operations/detach-and-reattach-motherduck-database/) the conflicting database on the service account. See [share alias conflicts](/sql-reference/motherduck-sql-reference/attach/#share-alias-conflicts) for details. |
| Iframe does not load (blank or blocked) | Your site's CSP blocks `embed-motherduck.com` | Add `frame-src https://embed-motherduck.com` to your CSP header (visible in browser dev console as a CSP violation) |
| User role "restricted" does not meet minimum role "admin" required for dashboards.createEmbedSession" | The user associated with the token is not an admin. Generating embed tokens requires the user or service account to have admin permissions. | In the service accounts panel under settings, change the role of the service account to 'Admin' |
| unauthorized_client: Callback URL mismatch. `<url>` is not in the list of allowed callback URLs | Embedded dives use MotherDuck's authorization system to determine permissions this limits what URLs can be used for authorization. | For local development ensure that you are running on `localhost` not something like `127.0.0.1` |

## Related resources

- [Creating visualizations with Dives](/key-tasks/ai-and-motherduck/dives/)
- [Dives SQL functions](/sql-reference/motherduck-sql-reference/ai-functions/dives/)
- [Managing Dives as code](/key-tasks/ai-and-motherduck/dives/managing-dives-as-code)
