Skip to the content.

MotherDuck Dives

A Dive is an interactive data app stored in MotherDuck: a React component that queries live data with the useSQLQuery hook and renders it with Recharts. MotherDuck exposes SQL functions for creating and versioning Dives, and duckdb_sqlalchemy wraps each one as a SQLAlchemy table function. That lets a Python application or a CI job build the data a Dive reads and deploy the Dive itself from the same code.

The complete workflow runs in examples/motherduck_dive.py.

Write the component

The component is a single default export. Declare the databases it reads in required_resources, and refer to them by alias with fully qualified, double-quoted names, so the Dive runs in any database context:

import { useSQLQuery } from "@motherduck/react-sql-query";
import { Line, LineChart, ResponsiveContainer, XAxis, YAxis } from "recharts";

export default function SalesOverview() {
  const { data, isLoading } = useSQLQuery(`
    SELECT ordered_on::VARCHAR AS day, sum(amount) AS revenue
    FROM "sales"."main"."orders"
    GROUP BY ALL ORDER BY day
  `);
  if (isLoading) return <p>Loading…</p>;
  const rows = Array.isArray(data) ? data : [];
  return (
    <ResponsiveContainer width="100%" height={320}>
      <LineChart data={rows.map((r) => ({ day: r.day, revenue: Number(r.revenue) }))}>
        <XAxis dataKey="day" />
        <YAxis />
        <Line dataKey="revenue" dot={false} />
      </LineChart>
    </ResponsiveContainer>
  );
}

data is the row array itself and is undefined while the query runs, so guard it before mapping. MotherDuck stores the content as given and does not check that it renders, so open a new Dive in the MotherDuck UI before sharing it.

Deploy from Python

Keep the component in your repository and deploy it with one function that creates the Dive on the first run and publishes a new version only when the content changed:

from sqlalchemy import select

from duckdb_sqlalchemy import (
    create_motherduck_engine,
    md_create_dive,
    md_get_dive,
    md_list_dives,
    md_update_dive_content,
)


def deploy_dive(conn, *, title, content, resources):
    dives = md_list_dives()
    dive_id = conn.execute(
        select(dives.c.id).where(dives.c.title == title)
    ).scalar_one_or_none()
    if dive_id is None:
        created = conn.execute(
            select(md_create_dive(title=title, content=content, required_resources=resources))
        ).one()
        return str(created.id), created.current_version

    current = conn.execute(select(md_get_dive(id=str(dive_id)))).one()
    if current.content == content:
        return str(dive_id), current.current_version
    version = conn.execute(
        select(
            md_update_dive_content(
                id=str(dive_id), content=content, required_resources=resources
            ).c.version
        )
    ).scalar_one()
    return str(dive_id), version


engine = create_motherduck_engine(database="md:")
with engine.begin() as conn:
    dive_id, version = deploy_dive(
        conn,
        title="Sales overview",
        content=open("dives/sales_overview.jsx").read(),
        resources=[{"url": "md:analytics", "alias": "sales"}],
    )

Pass the full required_resources list with every version: MotherDuck does not carry it over from the previous version. A resource is a database (md:<database>) or a share (md:_share/<name>/<uuid>).

Review and release versions

Every content change creates a numbered version. Mark the version you have reviewed so viewers know which one to trust. The status is one of draft, ready, endorsed or archived:

from duckdb_sqlalchemy import md_list_dive_versions, md_update_dive_status

with engine.begin() as conn:
    conn.execute(select(md_update_dive_status(id=dive_id, status="ready", version=version)))

    history = md_list_dive_versions(id=dive_id)
    for row in conn.execute(select(history.c.version, history.c.created_at)):
        print(row.version, row.created_at)

Store the approved version number with your release configuration when the Dive is embedded, and pass it when creating embed sessions (below), so a later edit in the UI does not change what customers see.

Feed Dives with fresh data

A Dive reads whatever the database holds when it runs, so the job that loads the data and the job that deploys the Dive can share the same engine. For Dives that many people open at once, serve the data from read scaling replicas: run CREATE SNAPSHOT OF <database> after your load commits so the replicas pick up the new data right away.

Embed a Dive in your application

Embedded Dives run as a MotherDuck service account. Your backend asks the MotherDuck REST API for a short-lived embed session, and your frontend renders it in a sandboxed iframe. This uses the REST API rather than SQL, so it is not part of this package. A minimal backend call with requests:

import os

import requests

response = requests.post(
    f"https://api.motherduck.com/v1/dives/{dive_id}/embed-session",
    headers={"Authorization": f"Bearer {os.environ['MOTHERDUCK_TOKEN']}"},
    json={"username": "tenant_a_service_account", "version": 2},
    timeout=30,
)
response.raise_for_status()
session = response.json()["session"]
<iframe
  src="https://embed-motherduck.com/sandbox/#session=<session>"
  sandbox="allow-scripts allow-same-origin"
  width="100%" height="600" style="border: none;"
></iframe>

The session contains a read-only token, expires after 24 hours, and must be treated as a credential: do not log or store it. Use one service account per tenant when tenants must not see each other’s data. See Embedding Dives for the full set of session options.

Helper reference

Each Dive SQL function has a helper that returns a SQLAlchemy table function. Nothing runs until the statement executes:

Helper Purpose
md_list_dives List Dives with their status and current version
md_get_dive Fetch one Dive, including its current content
md_create_dive Create a Dive and its first version
md_update_dive_content Publish a new version
md_update_dive_metadata Change the title or description without a new version
md_update_dive_status Set draft, ready, endorsed or archived for a version
md_list_dive_versions List the version history
md_get_dive_version Fetch one version, including its content
md_delete_dive Delete the Dive and its version history
from duckdb_sqlalchemy import (
    md_delete_dive,
    md_get_dive_version,
    md_update_dive_metadata,
)

renamed = md_update_dive_metadata(
    id="00000000-0000-0000-0000-000000000000",
    title="Sales overview (EMEA)",
    description="Weekly revenue by region",
)
rename_stmt = select(renamed.c.title, renamed.c.description)

version = md_get_dive_version(id="00000000-0000-0000-0000-000000000000", version=2)
version_stmt = select(version.c.version, version.c.content)

deleted = md_delete_dive(id="00000000-0000-0000-0000-000000000000")
delete_stmt = select(deleted.c.success)

The status columns need DuckDB 1.5.4 or newer, and md_list_dives returns required_resources from DuckDB 1.5.2. See columns by DuckDB version for details.