---
sidebar_position: 5
title: Embedding Dives in your website
description: Embed interactive MotherDuck Dives in your own website 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 website 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/) 
- A Dive you want to embed, with its [data shared](/sql-reference/mcp/share-dive-data) to your Service Account
- A backend server that can make authenticated API calls

## 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: {
      Authorization: `Bearer ${MOTHERDUCK_TOKEN}`,
      "Content-Type": "application/json",
    },
    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. Dual mode requires additional HTTP header configuration, and 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 and set the `allow` attribute for cross-origin isolation:

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

Dual mode also requires your page to send the following HTTP headers:

```text
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
```

Requiring all resources to have a [Cross-Origin-Resource-Policy (CORP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Embedder-Policy) means the browser knows it contains only resources that have actively consented to be shared. If you have third party resources like analytics tooling, advertisement scripts or payment integrations that do not set the Cross-Origin-Resource-Policy to `cross-origin` or `same-site`
these resources might be blocked on your page by the browser.

::::warning
When setting the [`Cross-Origin-Embedder-Policy`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Embedder-Policy) to `require-corp`, existing third party scripts, like analytics or advertising scripts on your site may be blocked if they do not have the required `Cross-Origin-Resource-Policy`.  Always verify this behaviour before you deploy.
::::

#### 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/) with an access token specifically for embedding, separate from your personal account. Note that the service account does need admin level access to generate the embed session strings.
- **Use a dedicated service account.** Create a [service account](/key-tasks/service-accounts-guide/) specifically for embedding, separate from your personal account. The service 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 |
| 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) |

## 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)
