THE DVT STYLE GUIDE
DVT projects are organized as a medallion architecture— bronze, silver, gold — the layering every data engineer already knows. What DVT changes is where a medallion can live: you don't need a lakehouse or a rented cloud warehouse to have one. Bronze stays inside the engines you already own, and silver and gold accumulate on one default target — any engine DVT connects. This page is the whole convention; our own cross-engine test project follows it exactly.
One default target — by design
Everything in a DVT project eventually lands in one place: the default target in your profile. That is the point — the default target isyour warehouse, whether it's Databricks, PostgreSQL, or anything else DVT speaks. Reads fan out across engines; writes converge on one.
Custom targets are the exception, not the architecture. target= on a model is fully supported and battle-tested — use it when a specific output genuinely must live elsewhere (an extract handed to another team, a file dropped on S3). But scattering models across engines by default fragments your warehouse and your lineage. In large projects — exactly the ones DVT is designed to carry — keep one default target and treat every target= as a decision that needs a reason.
Bronze — raw data, where it already lives
In a lakehouse, bronze means copying raw data into the lake before you can touch it. In DVT, bronze is declared, not ingested: sources.yml + meta.connectionmake every engine's raw tables addressable in place. Zero movement, zero duplication, zero ingestion pipelines to babysit.
- meta.connection on every source — even ones sitting on the default target. It costs nothing and it means switching your default target never breaks a read path.
- group sources by origin system — one yml block per system (coke, cbs, salesforce); table names stay exactly what the system calls them. Bronze never lies about the source.
- bronze models are optional 1:1 views — when a raw table needs light conformance (rename, cast, trim) before silver, give it a bronze_<entity> model. One source table, one bronze model, nothing else in it.
- connection names: <engine>_<env> — pg_dev, sf_prod, dbx_uat — profiles.yml outputs are connections; name them like the infrastructure they are.
Silver — cleaned and integrated, on the default target
Silver is where cross-engine data becomes one dataset: deduplicated, conformed, joined. A silver model reading foreign engines is federated by nature — declare materialized='f_table' (or f_incremental with a unique_key) and it lands on the default target like everything else.
- silver_<entity>[_<step>] — silver_transactions, silver_officers_enriched, silver_dept_acct_officer_cleaned. The layer prefix says exactly where you are in the flow.
- declare f_table / f_incremental explicitly — coercion will rescue a plain model that reads federated sources — with a warning — but in a maintained project the config block is documentation.
- ephemeral is allowed in silver — intermediate steps nobody queries can be ephemeral — and they still stay queryable through the api-portal, because DVT virtualizes logic, not tables.
Gold — dims and facts, business-ready
Gold is classic dimensional modeling — the vocabulary every engineer and BI tool already speaks. No layer prefix needed: the names are the convention.
- dim_<entity> and fact_<event> — dim_customers, dim_officers, fact_transactions, fact_top_category. If a BI developer can't guess what it is from the name, rename it.
- tables (or f_incremental), never views — gold is what dashboards hit; materialize it.
- descriptions written for consumers — a gold model's description is what an API consumer reads in the portal and an analyst reads in the catalog. Write it for them.
- API endpoints: kebab-case, <entity>-<audience> — orders-eu, officers-active. Locked filters carry the row-level rule; the name carries who it's for.
The folder tree
models/
sources.yml # bronze declarations: every engine, meta.connection
bronze/
coke/ # one folder per source system
bronze_customers_db1.sql
bronze_transactions_db1.sql
cbs/
bronze_dept_acct_officer.sql
bronze_country.sql
silver/
silver_transactions.sql
silver_officers_enriched.sql
silver_sales_by_category.sql
gold/
_gold_models.yml # descriptions + tests for the layer
dim_customers.sql
dim_officers.sql
fact_transactions.sqlLayer defaults live in dbt_project.yml (bronze/silver: views, gold: tables). YAML sits next to what it describes, underscore-prefixed so it sorts first. Delete the dbt starter example/ folder — always. And keep exactly one dbt_project.yml per repo: nested project files confuse every dbt-aware tool (IDE lineage extensions included).
The provenance header
In a single-engine project, a model's inputs and output are one guess away. In a federated project they aren't — so every model opens with a header answering the two questions a reader always has: where does this data come from, and where does it land?
-- MODEL: silver_officers_enriched
-- FROM: source snowflake_exim.cbs_f_country @ sf_dev (snowflake)
-- model silver_dept_acct_officer_current @ default target
-- TO: f_table @ default target
{{ config(materialized='f_table') }}
...One line per input — source, model or seed — with its connection and engine; one line for the landing spot. Generate these from the manifest rather than typing them: depends_on + meta.connection already know the truth, and hand-written headers drift.
Deliver where your consumers work
Your gold layer has customers: BI teams with their own databases, data scientists living in notebooks, applications that speak REST. DVT delivers to all of them without asking anyone to move. Add target= to a model and its result ships to any connected engine or bucket — computed once by your project, arriving as a native table the consuming team queries with the tools they already have. No pipelines to build on their side, no credentials into your warehouse, no exports to babysit.
-- the BI team's own PostgreSQL gets a real table, refreshed by your DAG
{{ config(materialized='f_table', target='analytics_pg') }}
-- the data-science bucket gets Parquet, ready for the next notebook
{{ config(materialized='f_table', target='s3_datasets') }}- native tables in their engine — a model can read five engines and land the finished result in a consumer's MySQL, SQL Server or anything else DVT speaks — to them it's just a table that's always fresh.
- datasets in the bucket — gold outputs land on S3, Azure, GCS or local disk as Parquet, CSV or JSON — instantly loadable by notebooks, pipelines and ML jobs.
- live APIs for applications — the api-portal serves the same models over REST as live virtual views — no materialization involved at all.
- governed like a product — keep the default target as home base and give every target= an owner and a reason. The catalog and each model's provenance header always show exactly where data lands.
House rules (dbt's, still true)
- snake_case everywhere — models, columns, sources.
- renames happen at bronze — downstream layers inherit clean names; a rename in silver or gold is a smell.
- test the boundaries — unique + not_null on every key in silver and gold. Tests on foreign-connection sources are skipped with a warning — test the bronze model instead.
- no SELECT * past bronze — columns are contracts. The api-portal's column governance works because the catalog knows exactly what a model serves.
- unique_key on every f_incremental — deltas apply as engine-native upserts; a keyless incremental is refused rather than silently full-refreshed.
We eat this cooking. DVT's own cross-engine test project — 11 engines, 85 models, every federation path — is organized exactly as this page describes: bronze/coke, bronze/cbs, silver/, gold/ — one default target, provenance headers on every model, green across the full DAG.