See it as a diagram
Everything below, as a diagram you can edit. Describe yours and see it in seconds.
No account needed · Editable canvas, not a picture
What dbt docs generate actually produces
The command writes two files into target/. manifest.json is the compiled project graph: every model, source, test and exposure with its dependencies, built from your code without touching the warehouse. catalog.json is the warehouse side: dbt queries the adapter for the actual tables, columns and types that exist. The docs site, target/index.html, is a single page that reads both files, which is why the lineage graph and the column lists can disagree with production the moment either the code or the warehouse moves on.
That mechanism defines the staleness problem precisely. Docs generated from a laptop describe that laptop's branch and that day's warehouse. The fix is not discipline, it is moving the generate step somewhere that runs on every merge.
Host the docs site from CI on every merge
Run dbt docs generate against production in the deploy job and publish target/ as a static site: GitHub Pages, Netlify, an S3 bucket behind your SSO, anything that serves three files. dbt Cloud does the hosting automatically on scheduled runs if you are there; self-hosted teams get the same result with one CI job. The property that matters is that nobody generates docs by hand anymore, so the site can no longer describe a stale branch.
One caution before publishing anywhere public: manifest.json embeds your model SQL and metadata, so treat the docs site as internal by default and put it behind access control rather than on the open web.
Push descriptions into the warehouse with persist_docs
A description that lives only in the docs site misses the analysts who never open it. The persist_docs config makes dbt write your model and column descriptions into the warehouse as native comments on every run, so they surface in the query editor, in information_schema, and in any catalog that crawls the warehouse. Exposures close the other end: declaring the dashboard or application that consumes your marts puts the downstream dependency into the lineage graph, so impact analysis stops ending at the last model.
# dbt_project.yml: descriptions become warehouse comments on every run
models:
my_project:
+persist_docs:
relation: true
columns: true
# models/exposures.yml: the dashboard enters the lineage graph
exposures:
- name: revenue_dashboard
type: dashboard
owner:
name: Analytics team
email: [email protected]
depends_on:
- ref('fct_orders')
- ref('dim_customers')Test description coverage instead of hoping for it
Coverage regresses one undocumented model at a time, so put it in CI where regressions get caught. dbt-checkpoint ships pre-commit hooks such as check-model-has-description and check-column-desc-are-same that fail the run when a model or column ships without documentation. The dbt_project_evaluator package takes the audit approach: it builds fact tables like fct_undocumented_models inside your project so you can query the debt and track it down over time.
The pragmatic policy for an existing project is a ratchet: enforce descriptions on new and changed models only, so the gate never demands a documentation sprint, it just refuses to let coverage get worse.
Minimum viable gate
The lineage view your stakeholders actually open
The dbt docs lineage graph is developer-grade: complete, unstyled, and overwhelming for anyone who does not know what a staging model is. For architecture reviews and stakeholder pages, teams keep a curated lineage diagram: the zones, the marts that matter, the exposures, without four hundred staging nodes. Generate the first version by pasting your model list or manifest excerpt into the dbt lineage diagram generator, and keep it current the same way you keep the code current: an agent connected to the Datadef MCP server, registry name io.datadef/mcp, reads manifest.json after a merge and updates the diagram. Datadef does not watch the repo; the update is one agent call or CI step, and the API key it uses comes from a paid plan. The full loop is in living diagram from dbt.
Shared public, the diagram serves a permanent image URL that re-renders on change, so the copy embedded in your README or wiki follows within minutes. And to keep scope honest: this loop keeps the diagram current. The YAML descriptions themselves are prose, and writing them stays with your team; dbt best practices covers the conventions that make that sustainable.
FAQ
How do I keep dbt docs up to date?
Does dbt docs update automatically?
What is the difference between manifest.json and catalog.json?
How do I enforce documentation coverage in a dbt project?
What does persist_docs actually do?
Why declare dbt exposures?