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 sourceeven 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 systemone 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 viewswhen 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 explicitlycoercion 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 silverintermediate 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 viewsgold is what dashboards hit; materialize it.
  • descriptions written for consumersa 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.sql

Layer 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 enginea 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 bucketgold 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 applicationsthe api-portal serves the same models over REST as live virtual views — no materialization involved at all.
  • governed like a productkeep 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 everywheremodels, columns, sources.
  • renames happen at bronzedownstream layers inherit clean names; a rename in silver or gold is a smell.
  • test the boundariesunique + 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 bronzecolumns are contracts. The api-portal's column governance works because the catalog knows exactly what a model serves.
  • unique_key on every f_incrementaldeltas 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.