ServicesAboutNotesContact Get in touch →
EN FR
Note

dbt Groups and Access Modifiers

How dbt groups and access modifiers (private, protected, public) organize model ownership and enforce boundaries — and why they're worth using even in single projects.

Planted
dbtdata engineeringdata modeling

Groups and access modifiers were introduced in dbt Core v1.5 alongside model contracts. They address two governance questions: who owns a set of models, and who is allowed to depend on them. Most obviously useful in dbt Mesh (multi-project setups), they also provide value in single-project setups.

What Groups Are

A group is a named collection of models with a declared owner. Groups live in dbt_project.yml:

groups:
- name: marketing
owner:
email: marketing-data@company.com
- name: finance
owner:
email: finance-data@company.com

You assign models to groups at the folder level:

models:
my_project:
marts:
marketing:
+group: marketing
finance:
+group: finance

That’s all a group is: a way to record that a set of models belongs to a team and someone specific is responsible for them.

What Access Modifiers Are

Access modifiers control which models outside a group can reference a model:

  • private: Only models within the same group can reference this model. Everything else gets a compilation error.
  • protected (the default): Any model in the same project can reference this model. Models in other projects cannot.
  • public: Any model — in this project or any other — can reference this model.
models:
my_project:
marts:
marketing:
+group: marketing
+access: public
intermediate:
+access: protected # Default, but explicit
base:
+access: private

In practice: public marts are the interface your organization sees. Protected intermediates are internal implementation details that other teams within the project can use if they need to. Private base models belong entirely to the layer responsible for cleaning that source.

Value in single-project setups

Three reasons to use groups and access modifiers in single-project setups:

Documentation

Groups record ownership in a machine-readable way. The answer to “who owns the finance mart models?” is in dbt_project.yml, not a wiki. As teams grow, the person who built a model months ago may have moved on — groups make ownership an artifact of the project rather than knowledge held in someone’s head.

Future-proofing for Mesh

If your project ever grows to the point where splitting by domain makes sense, the groups structure is already in place. You’ve already identified the boundaries, declared the owners, and marked which models are public contracts versus internal implementation. The migration to Mesh becomes surgical rather than architectural.

The inverse is painful: trying to introduce Mesh into a project that was built without any concept of ownership means retroactively deciding what’s public and what isn’t, potentially breaking a lot of implicit dependencies.

Enforced architectural boundaries

Without access modifiers, nothing stops a motivated engineer from referencing base__ga4__event directly in a mart. The mart bypasses your intermediate layer, duplicates logic, and creates a hidden dependency that breaks when the base model changes.

With +access: private on your base layer, dbt will refuse to compile a mart that tries to reference a base model. The architecture becomes self-enforcing. You don’t need code review to catch these violations; the build fails.

This is the same value as lint rules or architectural tests — it converts a social convention into a technical constraint.

Access Modifiers in Practice

The typical setup for a three-layer dbt project:

dbt_project.yml
groups:
- name: marketing
owner:
email: marketing-data@company.com
- name: finance
owner:
email: finance-data@company.com
- name: data-platform
owner:
email: data-platform@company.com
models:
my_project:
base:
+group: data-platform
+access: private # Only platform team, only through intermediate
intermediate:
+group: data-platform
+access: protected # Project-wide, but not cross-project
marts:
+materialized: table
marketing:
+group: marketing
+access: public # Stable contracts for dashboards and consumers
finance:
+group: finance
+access: public

Mart models marked public are the contract with downstream consumers. Combined with model contracts, a public mart with contract: {enforced: true} guarantees both that the model is accessible and that its schema is stable.

The Mesh Connection

In a dbt Mesh setup, access: public is what makes cross-project references possible at all. Another dbt project can only reference models explicitly marked public. Combined with model versions, this creates a proper API: the model is public, it’s versioned, it has a schema contract, and it has a declared owner.

For single projects, public still has meaning — it signals intent. A public model is one where you’re committing to schema stability. Other teams query it in BI tools. Pipelines depend on it. Changing it requires backward compatibility consideration or a versioned migration.

This is worth being explicit about even when there’s no technical enforcement. The discipline of deciding “is this model a public contract or an internal implementation?” improves the design of your project, because it forces you to think about boundaries.

Starting point

  1. Add groups for each team that owns models (even if it’s just one team)
  2. Mark mart models +access: public
  3. Leave everything else at the default protected

Do not add private to base models immediately — this can break existing models if cross-layer dependencies exist that were not previously surfaced. Add private incrementally after confirming no out-of-layer references exist.