Embedding Dives in your web application
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
- A Dive you want to embed, with its data shared to the target service account that the embedded Dive will run as
- A backend server that can make authenticated API calls
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:
- 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.
- Your frontend renders a sandboxed iframe that loads the Dive from
embed-motherduck.com, passing the session string. - MotherDuck loads the Dive and runs live SQL queries.
Your end-users see an interactive dashboard without needing a MotherDuck account.
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.
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.
Never expose your access token in client-side code. The access token stays on your backend. Only the session string reaches the browser.
- Node.js
- Python
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
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
Replace <your_dive_id> with the ID of your Dive. You can find this in Settings > Dives or through the 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:
<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:
<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:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Requiring all resources to have a Cross-Origin-Resource-Policy (CORP) 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.
When setting the 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 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/user account. Create a service account 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, add embed-motherduck.com to your frame-src directive:
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 |
| "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 the conflicting database on the service account. See 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) |