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.comYou assign models to groups at the folder level:
models: my_project: marts: marketing: +group: marketing finance: +group: financeThat’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: privateIn 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:
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: publicMart 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
- Add groups for each team that owns models (even if it’s just one team)
- Mark mart models
+access: public - 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.