Master dbt™ database configuration: understand cross-database writes, prevent dev/prod conflicts, and configure databases properly across environments.
Fabio Di Leta
·
Nov 21, 2025
·
6
min read
dbt™ database configuration looks simpler than schemas. It's not. You can accidentally write to production from dev, break permissions, or deploy code that works in CI but fails in prod.
Run dbt run --target dev and every model lands in analytics_dev by default.
That's your base database. Everything else builds from here.
Target Database vs Custom Database
Two different things. Here's what they actually are.
Target database: Where dbt™ puts your models by default. Set in profiles.yml:
my_project:
target: dev
outputs:
dev:
database: analytics_dev # ← This is your target database
my_project:
target: dev
outputs:
dev:
database: analytics_dev # ← This is your target database
my_project:
target: dev
outputs:
dev:
database: analytics_dev # ← This is your target database
Run dbt run --target dev and every model lands in analytics_dev. That's it.
Custom database: Your override. Set in the model:
{{ config(database='data_science')}} # ← This is your custom database
select * from{{ref('base_model')}}
{{ config(database='data_science')}} # ← This is your custom database
select * from{{ref('base_model')}}
{{ config(database='data_science')}} # ← This is your custom database
select * from{{ref('base_model')}}
When you set a custom database, dbt™ uses it directly. No concatenation. No prefix. Just switches databases.
Example:
Target database: analytics_dev
Custom database: data_science
Where the model lands: data_science.dbt_john.my_model
Not analytics_dev_data_science. Just data_science.
Why no concatenation? Because database names need to be exact. You can't just make up database names. They have to exist and you need grants on them.
But this creates a problem. A big one.
The Problem with Custom Databases
Your model has database='data_science' in the config. Works in prod. Fails in dev.
Why? Your dev user doesn't have write access to data_science. Or worse - you accidentally write to the production data_science database from your laptop.
Both bad.
Here's what happens by default:
Environment
Target Database
Custom Database Config
Where Model Lands
Production
analytics_prod
data_science
data_science.analytics.my_model ✅
Developer - John
analytics_dev
data_science
data_science.dbt_john.my_model ❌
Developer - Jane
analytics_dev
data_science
data_science.dbt_jane.my_model ❌
See it? Everyone hits the same data_science database. If it's the prod database, you just wrote dev data to prod. If it's a database you don't have access to, your run fails.
You need environment-specific logic.
Understanding Database Behavior: No Concatenation
Unlike schemas, databases don't concatenate. The generate_database_name macro works differently:
{% macro generate_database_name(custom_database_name, node) -%}{%- set default_database = target.database -%}{%- if custom_database_name isnone -%}{{ default_database }}{%- else -%}{{ custom_database_name | trim }}{%- endif -%}{%- endmacro %}
{% macro generate_database_name(custom_database_name, node) -%}{%- set default_database = target.database -%}{%- if custom_database_name isnone -%}{{ default_database }}{%- else -%}{{ custom_database_name | trim }}{%- endif -%}{%- endmacro %}
{% macro generate_database_name(custom_database_name, node) -%}{%- set default_database = target.database -%}{%- if custom_database_name isnone -%}{{ default_database }}{%- else -%}{{ custom_database_name | trim }}{%- endif -%}{%- endmacro %}
No underscore. No concatenation. If you set a custom database, dbt™ uses it directly.
This is why you need to override it.
Controlling Database Behavior: Development vs Production
Production needs cross-database writes. Dev doesn't (and shouldn't).
Create macros/get_custom_database.sql:
{% macro generate_database_name(custom_database_name, node) -%}{%- set default_database = target.database -%}{%- if target.name == 'prod' -%}{%- if custom_database_name isnone -%}{{ default_database }}{%- else -%}{{ custom_database_name | trim }}{%- endif -%}{%- else -%}{# In dev, always use dev database #}{{ default_database }}{%- endif -%}{%- endmacro %}
{% macro generate_database_name(custom_database_name, node) -%}{%- set default_database = target.database -%}{%- if target.name == 'prod' -%}{%- if custom_database_name isnone -%}{{ default_database }}{%- else -%}{{ custom_database_name | trim }}{%- endif -%}{%- else -%}{# In dev, always use dev database #}{{ default_database }}{%- endif -%}{%- endmacro %}
{% macro generate_database_name(custom_database_name, node) -%}{%- set default_database = target.database -%}{%- if target.name == 'prod' -%}{%- if custom_database_name isnone -%}{{ default_database }}{%- else -%}{{ custom_database_name | trim }}{%- endif -%}{%- else -%}{# In dev, always use dev database #}{{ default_database }}{%- endif -%}{%- endmacro %}
Run dbt compile and watch what values get passed in.
Test permissions
dbt run --select my_model --target prod --dry-run
dbt run --select my_model --target prod --dry-run
dbt run --select my_model --target prod --dry-run
Best Practices
Override generate_database_name for dev environments. The default behavior is dangerous.
Production should allow custom databases. Dev should ignore them or prefix them.
Document which databases exist and what they're for. Drop a note in the project README.
Use database configs sparingly. Most projects don't need them. Schemas are usually enough.
What Not to Do
Don't Allow Cross-Database Writes in Dev
Some people want feature parity between dev and prod:
{% macro generate_database_name(custom_database_name, node) -%}{%- set default_database = target.database -%}{%- if custom_database_name isnone -%}{{ default_database }}{%- else -%}{{ custom_database_name | trim }} # ❌ Works inall environments
{%- endif -%}{%- endmacro %}
{% macro generate_database_name(custom_database_name, node) -%}{%- set default_database = target.database -%}{%- if custom_database_name isnone -%}{{ default_database }}{%- else -%}{{ custom_database_name | trim }} # ❌ Works inall environments
{%- endif -%}{%- endmacro %}
{% macro generate_database_name(custom_database_name, node) -%}{%- set default_database = target.database -%}{%- if custom_database_name isnone -%}{{ default_database }}{%- else -%}{{ custom_database_name | trim }} # ❌ Works inall environments
{%- endif -%}{%- endmacro %}
Bad idea. Here's what happens:
Environment
Target Database
Custom Database
Result
Production
analytics_prod
data_science
data_science ✅
Developer - John
analytics_dev
data_science
data_science ❌
Developer - Jane
analytics_dev
data_science
data_science ❌
CI PR 123
analytics_ci
data_science
data_science ❌
Everyone writes to the same database. Developer 1 and Developer 2 overwrite each other. CI builds collide. If data_science is the prod database, you just mixed dev data into prod.
Fix: Block custom databases in dev.
{% macro generate_database_name(custom_database_name, node) -%}{%- set default_database = target.database -%}{%- if target.name == 'prod' -%}{%- if custom_database_name isnone -%}{{ default_database }}{%- else -%}{{ custom_database_name | trim }} # Custom databases work{%- endif -%}{%- else -%}{{ default_database }} # Force dev database
{%- endif -%}{%- endmacro %}
{% macro generate_database_name(custom_database_name, node) -%}{%- set default_database = target.database -%}{%- if target.name == 'prod' -%}{%- if custom_database_name isnone -%}{{ default_database }}{%- else -%}{{ custom_database_name | trim }} # Custom databases work{%- endif -%}{%- else -%}{{ default_database }} # Force dev database
{%- endif -%}{%- endmacro %}
{% macro generate_database_name(custom_database_name, node) -%}{%- set default_database = target.database -%}{%- if target.name == 'prod' -%}{%- if custom_database_name isnone -%}{{ default_database }}{%- else -%}{{ custom_database_name | trim }} # Custom databases work{%- endif -%}{%- else -%}{{ default_database }} # Force dev database
{%- endif -%}{%- endmacro %}
Production gets cross-database writes. Dev stays isolated. Problem solved.
Don't Skip the Macro Override
"I'll just be careful" doesn't work. Someone will run dbt run against the wrong target. Someone will forget to check their active profile. Someone will deploy code without testing.
The macro is your safety net. Use it.
Key Takeaways
Default behavior: dbt™ uses custom databases directly. No concatenation. This is dangerous in dev.
Override generate_database_name to control database behavior by environment.
Production gets cross-database writes. Dev ignores custom databases or prefixes them.
Configure at model, directory, or project level. Most specific wins.
Test permissions before deploying. Grant failures are common.
Interested to Learn More? Try Out the Free 14-Days Trial
*dbt® and dbt Core® are federally registered trademarks of dbt Labs, Inc. in the United States and various jurisdictions around the world. Paradime is not a partner of dbt Labs. All rights therein are reserved to dbt Labs. Paradime is not a product or service of or endorsed by dbt Labs, Inc.
*dbt® and dbt Core® are federally registered trademarks of dbt Labs, Inc. in the United States and various jurisdictions around the world. Paradime is not a partner of dbt Labs. All rights therein are reserved to dbt Labs. Paradime is not a product or service of or endorsed by dbt Labs, Inc.
*dbt® and dbt Core® are federally registered trademarks of dbt Labs, Inc. in the United States and various jurisdictions around the world. Paradime is not a partner of dbt Labs. All rights therein are reserved to dbt Labs. Paradime is not a product or service of or endorsed by dbt Labs, Inc.