Three techniques make dbt models installable by anyone: configurable sources with var(), enable/disable flags, and namespaced model names. The Fivetran team’s approach to these patterns has become a de facto standard. Omitting any one of them will break installations in environments with different schema layouts or naming.
Configurable Sources with var()
Never hardcode where source data lives. What’s raw_stripe.payments in your warehouse is analytics.stripe_data.payment_events in someone else’s. The var() function with sensible defaults handles this cleanly.
Source Definition
sources: - name: my_package schema: "{{ var('my_package_schema', 'my_data') }}" database: "{{ var('my_package_database', target.database) }}" tables: - name: events identifier: "{{ var('my_package_events_identifier', 'events') }}"Three levels of configurability here:
- Schema — where the source tables live. Most users will need to change this.
- Database — defaults to the user’s target database. Only needed for cross-database setups.
- Identifier — the actual table name. Handles cases where a table has a different name in someone’s warehouse (
raw_eventsinstead ofevents).
Base Model
-- models/base/base__my_package__events.sql
WITH source AS (
SELECT event_id, event_name, event_timestamp, user_id FROM {{ source('my_package', 'events') }}
)
SELECT event_id, event_name, event_timestamp, user_idFROM sourceThe base model references source(), which resolves the schema, database, and identifier from the YAML definition. Users point the package at their own schema by setting my_package_schema in their dbt_project.yml:
# User's dbt_project.ymlvars: my_package_schema: 'raw_stripe' my_package_events_identifier: 'payment_events'This is the same base layer pattern used in regular projects, with var() wrapping the parts that differ across environments.
Variable Naming Convention
Follow the convention established by Fivetran and dbt Labs:
| Setting | Variable Name | Example |
|---|---|---|
| Schema | {package}_schema | my_package_schema |
| Database | {package}_database | my_package_database |
| Table identifier | {package}_{table}_identifier | my_package_events_identifier |
| Feature flag | {package}__{model}_enabled | my_package__daily_summary_enabled |
The double underscore before the model name in feature flags separates the package namespace from the setting name, avoiding ambiguity with multi-word package names.
Enable/Disable Models
Not every user needs every model in your package. Enable/disable flags let users turn off parts they don’t need, reducing build time and warehouse clutter.
-- models/marts/my_package__daily_summary.sql{{ config(enabled=var('my_package__daily_summary_enabled', true)) }}
SELECT DATE(event_timestamp) AS event_date, COUNT(DISTINCT user_id) AS unique_users, COUNT(*) AS total_eventsFROM {{ ref('base__my_package__events') }}GROUP BY 1The enabled config accepts a var() call directly. Default to true so models are included unless explicitly disabled. Users opt out in their dbt_project.yml:
vars: my_package__daily_summary_enabled: falseWhen a model is disabled, dbt skips it entirely — no compilation, no execution, no warehouse object. Downstream models that reference a disabled model will fail, so document which models are safe to disable independently. A common pattern is to group models into “modules” with a single flag:
vars: my_package__daily_models_enabled: true my_package__weekly_models_enabled: true my_package__monthly_models_enabled: false # Disable all monthly aggregationsNamespaced Model Names
Every model in your package should start with the package name. If your package is called revenue_tools, name your models revenue_tools__monthly_mrr and revenue_tools__churn_events, not monthly_mrr and churn_events.
This prevents naming collisions when users have multiple packages installed. Without prefixes, two packages that both define a customers model will conflict, and the error messages aren’t always clear about the cause.
The convention applies to all model types:
| Layer | Example |
|---|---|
| Base | base__my_package__events |
| Intermediate | my_package__enriched_events |
| Mart | my_package__daily_summary |
Base models follow the standard base naming convention (base__source__entity) with the package name as the source. Higher-layer models use the package name as a prefix directly.
This is verbose, but collisions between packages are one of the most frustrating debugging experiences in dbt. A user with fivetran_stripe, fivetran_shopify, and your revenue_tools package installed needs unambiguous model names.
Putting It Together
Here’s a complete source and base model setup for a package called ad_analytics:
sources: - name: ad_analytics schema: "{{ var('ad_analytics_schema', 'raw_ads') }}" database: "{{ var('ad_analytics_database', target.database) }}" tables: - name: campaigns identifier: "{{ var('ad_analytics_campaigns_identifier', 'campaigns') }}" - name: ad_spend identifier: "{{ var('ad_analytics_ad_spend_identifier', 'ad_spend') }}"-- models/base/base__ad_analytics__campaigns.sql
WITH source AS (
SELECT * FROM {{ source('ad_analytics', 'campaigns') }}
),
renamed AS (
SELECT campaign_id, campaign_name, platform, start_date, end_date, budget_amount, currency_code FROM source
)
SELECT * FROM renamed-- models/marts/ad_analytics__daily_spend.sql{{ config(enabled=var('ad_analytics__daily_spend_enabled', true)) }}
SELECT DATE(spend_date) AS spend_date, c.campaign_name, c.platform, SUM(s.amount) AS total_spend, SUM(s.impressions) AS total_impressions, SUM(s.clicks) AS total_clicksFROM {{ ref('base__ad_analytics__ad_spend') }} sLEFT JOIN {{ ref('base__ad_analytics__campaigns') }} c ON s.campaign_id = c.campaign_idGROUP BY 1, 2, 3# dbt_project.yml (package defaults)vars: ad_analytics_schema: 'raw_ads' ad_analytics_database: null ad_analytics__daily_spend_enabled: trueUsers customize with minimal configuration:
# User's dbt_project.ymlvars: ad_analytics_schema: 'my_company_ads'Everything else falls through to defaults. The user gets a working package with one line of configuration.