Blog

Why we built dbt-doctor

Why we built dbt-doctor, an offline static analysis tool that turns recurring dbt project issues into clear checks and a practical health score.

Joachim Hodana4 min read
Northgrain Data banner for dbt-doctor showing recurring dbt project findings, an offline terminal scan and a project health score

The dbt projects I reviewed tended to surface the same categories of issues. Missing contracts. Incremental models without a unique_key. Sources without freshness configuration. Model SQL that bypassed dbt lineage by referencing relations directly.

The findings were predictable. The tooling to catch them consistently was not.

The existing options each solve part of the problem. SQLFluff handles SQL style. dbt-checkpoint enforces pre-commit checks. dbt-score grades metadata quality. dbt-project-evaluator catches structural issues. Using all of them means maintaining separate configurations, separate rule sets, and several places where something can break in CI.

Most teams use one or two tools and leave the rest uncovered.

The gap was not another linter

The gap that bothered me most was not only fragmentation. It was that most static analysis output means very little outside the engineering team.

A 200-line linting report communicates nothing to an engineering lead deciding whether to invest in technical debt cleanup. A list of YAML violations means nothing to a client asking whether their dbt project is in good shape.

The inspiration came from React Doctor. One command, no application build required, and an immediate diagnosis with a clear health score. Applying that model to dbt seemed useful and missing.

dbt-doctor adapts that idea to dbt projects.

What dbt-doctor does differently

The design decisions came from two directions: what engineers need and what non-technical stakeholders can actually read.

Zero friction for engineers

The first requirement was a useful offline mode. dbt-doctor can inspect a project without a warehouse connection, dbt compilation, or credentials. That makes it useful immediately after cloning a repository, before the local environment is fully configured.

Run it from the dbt project root:

npx dbt-doctor@latest --offline

The offline flag skips the remote score API and share link. The project is still scanned and scored locally.

A score stakeholders can read

The second requirement was a result that works outside an engineering conversation. dbt-doctor returns a score from 0 to 100 with three labels:

  • Great: 75 or above
  • Needs work: 50 to 74
  • Critical: below 50

Engineers can inspect the detailed findings. Everyone else can understand whether project health is improving or degrading. "The project moved from 41 to 78" works in a delivery update as well as a technical retrospective.

The default scoring mode accounts for three things:

score = max(0, round(100 - rule penalty - file spread penalty - finding volume penalty))

Unique error rules cost more than warning rules. Problems spread across many files receive a larger penalty. Finding volume adds a capped penalty so one repeated issue does not dominate the score indefinitely.

This keeps the score focused on systemic quality while still reflecting the blast radius of the findings. Teams that want scoring focused on rule diversity can switch to unique-rules mode.

What it checks

The checks focus on maintainability, project structure, documentation, tests, source configuration, governance and SQL quality. The current catalog is published in the dbt-doctor rules reference.

A few checks came directly from recurring findings in real projects.

Models without enforced contracts

A column rename in a model feeding a dashboard can break downstream consumers without a clear boundary. The model-contract-enforced rule flags models that do not set contract.enforced: true in model YAML.

Dependencies that bypass dbt lineage

Hardcoded relation names make lineage and environment isolation harder to trust. The prefer-ref-over-raw-source rule detects model SQL that appears to reference a relation directly instead of using ref().

Incremental models without a unique key

Incremental merge strategies depend on a stable key. The incremental-unique-key rule flags incremental models that do not declare unique_key in SQL config or model YAML.

Sources without freshness configuration

Freshness monitoring needs explicit thresholds. The source-freshness rule flags declared sources without a freshness configuration and recommends adding warn_after and error_after thresholds.

SQL quality when the team wants it

dbt-doctor can run SQLFluff when it is available and can adopt an existing .sqlfluff or pyproject.toml configuration. Teams that only want the built-in structural checks can use skipSqlfluff or customRulesOnly.

CI integration

The GitHub Action was part of the product design. A check that only runs locally catches issues when someone remembers to run it. A check that runs on every pull request catches them before they reach production.

- uses: northgraindata/dbt-doctor@main
  with:
    diff: main
    github-token: ${{ secrets.GITHUB_TOKEN }}

With diff: main, only files changed against the base branch are scanned. With a GitHub token on a pull request, findings appear as a sticky PR comment. The action also exposes the health score for follow-up quality gates.

The complete inputs and outputs are documented in action.yml.

What comes next

The check library should expand from real findings, not from abstract ideas about what a good dbt project looks like.

The repository is open source at github.com/northgraindata/dbt-doctor. The rule catalog is generated from the implementation. Pull requests for useful checks, issues for false positives, and feedback from real projects are all valuable.

Run it on a dbt project first. If it catches something useful, a GitHub star helps more teams discover it.

Planning a data platform change?

Tell us what you are working with and what needs to work better.