Skip to main content

Build a MotherDuck Warehouse with dbt and Deploy a Dive

Build this recipe with your AI assistant

I want to build a MotherDuck warehouse from scratch with dbt-duckdb and then ship a Dive on top of the mart tables. Help me adapt the "Build a MotherDuck Warehouse with dbt and Deploy a Dive" recipe to my own data and use case, using it as a guide: https://motherduck.com/docs/cookbook/dbt-duckdb-dwh-starter

A minimal dbt-duckdb starter for building a MotherDuck warehouse end to end and shipping a Dive on top of it. It reads the Common Crawl domain hyperlink graph (which sites link to a target domain) straight from remote gzip files, joins it with the Hacker News dataset, and builds tested mart tables. A deploy script then publishes a Dive that visualizes the marts. It shows the MotherDuck pattern of turning large remote CSV/gzip sources into performant tables with dbt, combining datasets across a share, and deploying a Dive as code.

With this setup you'll learn how to:

  • Turn large CSV/gzip source data into performant MotherDuck tables
  • Combine data from different sources (Common Crawl + a Hacker News share) with dbt
  • Deploy a MotherDuck Dive from validated, tested marts

Backlinks and Hacker News Coverage Dive

How it works

The dbt project layers staging, intermediate, and mart models, each in its own schema suffix (<DBT_SCHEMA>_stg, _int, _mart):

  • models/staging/commoncrawl: source-backed tables over the Common Crawl remote gzip files for the configured commoncrawl_snapshot. The default snapshot cc-main-2026-jan-feb-mar exposes three outputs: domain-ranks (PageRank and harmonic centrality), domain-vertices (domain ids and reversed host names), and domain-edges (domain-to-domain links, filtered to your configured domains).
  • models/staging/hackernews: Hacker News stories. The model attaches the public Hacker News MotherDuck share before reading it:
  ATTACH IF NOT EXISTS 'md:_share/hacker_news/de11a0e3-9d68-48d2-ac44-40e07a1d496b' AS hacker_news;
  • models/intermediate: target-domain joins and edge expansion.
  • models/marts: tested tables ready to query, and the source the Dive reads.

The Dive lives in dives/backlinks-hn/dive.tsx and queries the mart tables. The deploy script (scripts/deploy-dive.sh) reads dives/<name>/dive-manifest.json, loads the dive.tsx source with DuckDB read_text(), substitutes the database and _mart schema into the source, and creates or updates the Dive in MotherDuck with the MD_CREATE_DIVE / MD_UPDATE_DIVE_CONTENT functions.

Questions to answer

  • Which target domain(s) should the link graph and HN coverage focus on (commoncrawl_domains)?
  • Which MotherDuck database and base schema, and dev or prod (DBT_DUCKDB_PATH, DBT_SCHEMA, --target)?
  • Which Common Crawl snapshot, and how many HN stories per domain (commoncrawl_snapshot, hackernews_max_stories_per_domain)?
  • Is the large Common Crawl edges download acceptable (see Caveats), or should the scope be narrowed first?
  • Deploy the Dive, and as a preview or to production?
  • Is a MotherDuck account and token available, with access to the Hacker News share?

Caveats

  • The Common Crawl edges file is large (~14GB). stg_commoncrawl__domain_edges is materialized incrementally so it is not re-downloaded on every run; avoid a casual --full-refresh of that model.
  • DBT_DUCKDB_PATH and DBT_SCHEMA must match between the dbt build and the Dive deploy. If they differ, the Dive points at a database/_mart schema that the build did not populate and renders empty.
  • The Hacker News staging model attaches a MotherDuck share (md:_share/hacker_news/...); the run needs access to that share.
  • scripts/deploy-dive.sh requires the duckdb CLI and jq on PATH and MOTHERDUCK_TOKEN set; it exits early if any are missing.
  • The deploy expects a unique Dive title: if more than one Dive already shares the title it errors instead of guessing which to update. Use PREVIEW_BRANCH for non-production deploys.
  • threads: 24 in profiles.yml is aggressive; lower it for smaller machines or plans.

What you'll adjust

SettingPurposeOptions / example
commoncrawl_domains (dbt_project.yml vars)The target domains the link graph and HN stories are filtered to.[motherduck.com, duckdb.org]; add your own domains
commoncrawl_snapshot (dbt_project.yml vars)Which Common Crawl web-graph snapshot to read.cc-main-2026-jan-feb-mar
hackernews_max_stories_per_domain (dbt_project.yml vars)Cap on HN stories pulled per domain.100
DBT_DUCKDB_PATH (env)MotherDuck database dbt builds into. Must match the Dive deploy.md:my_db (dev), md:dbt_prod (prod)
DBT_SCHEMA (env)Base schema; models land in <DBT_SCHEMA>_stg/_int/_mart. Must match the Dive deploy.dbt_dev (dev), dbt_main (prod)
dbt --target (profiles.yml)Which profile output to use.dev (default), prod
threads (profiles.yml)dbt thread count.24; lower it for smaller machines or plans
MOTHERDUCK_TOKEN (env)MotherDuck access token for dbt and the Dive deploy.a token from the MotherDuck UI
dives/backlinks-hn/dive.tsx + dive-manifest.jsonThe Dive's React/SQL source and its title/description.edit tiles, queries, title
PREVIEW_BRANCH (env, deploy)Appends a branch name to the Dive title so a preview does not overwrite production.$(git branch --show-current)

Run it

Prerequisites: a MotherDuck account and access token, and uv. The Dive deploy also needs the duckdb CLI and jq on your PATH.

uv sync
export MOTHERDUCK_TOKEN="..."

# Validate, then build the warehouse (dev target by default)
uv run dbt debug --profiles-dir .
uv run dbt parse --profiles-dir .
uv run dbt build --profiles-dir .

Domains are configured in dbt_project.yml under commoncrawl_domains; add more domains there as needed. Create the target MotherDuck database first if it does not already exist.

Deploy the dive

Build the dbt project first, then deploy the Dive. DBT_DUCKDB_PATH and DBT_SCHEMA must match the values used for the build, because the deploy script substitutes the database and ${DBT_SCHEMA}_mart schema into the Dive source:

export MOTHERDUCK_TOKEN="..."
export DBT_DUCKDB_PATH="md:my_db"
export DBT_SCHEMA="dbt_dev"
uv run dbt build --profiles-dir .
./scripts/deploy-dive.sh backlinks-hn

For production, use your prod database and schema for both the build and deploy:

export DBT_DUCKDB_PATH="md:dbt_prod"
export DBT_SCHEMA="dbt_main"
uv run dbt build --target prod --profiles-dir .
./scripts/deploy-dive.sh backlinks-hn

For a preview that does not overwrite the production Dive, set PREVIEW_BRANCH; the script appends the branch name to the Dive title:

PREVIEW_BRANCH="$(git branch --show-current)" ./scripts/deploy-dive.sh backlinks-hn

The script prints the deployed Dive URL.

Files

  • dbt_project.yml - project config and the vars knobs (commoncrawl_domains, commoncrawl_snapshot, hackernews_max_stories_per_domain) plus per-layer schema and materialization.
  • profiles.yml - dev and prod DuckDB outputs reading DBT_DUCKDB_PATH / DBT_SCHEMA from the environment; the token comes from MOTHERDUCK_TOKEN.
  • models/staging/ - source-backed tables: commoncrawl/ (the three Common Crawl outputs over remote gzip, edges materialized incrementally) and hackernews/ (reads the attached Hacker News share).
  • models/intermediate/ - target-domain joins and edge expansion.
  • models/marts/ - tested mart tables the Dive queries (mart_domain_backlinks, mart_domain_link_graph, mart_hackernews_domain_stories).
  • macros/commoncrawl.sql - helpers for building the Common Crawl source URLs and reads.
  • dives/backlinks-hn/ - the Dive: dive.tsx (React + SQL source with __DBT_DATABASE__ / __DBT_MART_SCHEMA__ placeholders) and dive-manifest.json (title, description, source file).
  • scripts/deploy-dive.sh - deploys a Dive from dives/<name>/ using the DuckDB CLI and the MD_CREATE_DIVE / MD_UPDATE_DIVE_CONTENT functions.
  • pyproject.toml / uv.lock - Python dependencies (dbt-duckdb) managed with uv.
  • assets/backlinks-hn-dive.png - screenshot of the deployed Dive.

Learn more