Medallion Architecture on Snowflake with Atlas and dbt
In a Snowflake medallion architecture, raw data lands in Bronze, which dbt cleans into Silver and shapes
into Gold. dbt owns the two upper layers completely. Every dbt run recreates the tables and views its
models define, from code that is versioned, reviewed, and tested.
The bronze layer those models stand on has no such owner. This post is about that layer, and giving Bronze a proper owner without disrupting your dbt project.
The Layer Nobody Owns
What would happen if your Snowflake account disappeared tomorrow?
Silver and Gold could be rebuilt entirely from your dbt project, with version-controlled models, tests, and a DAG.
Bronze is not so simple. The raw data arrives from ingestion, but the landing tables, file formats, and schema beneath it were built by hand.
The layer beneath all three does not answer at all. Your schemas, role hierarchies, and access grants
exist because someone opened a worksheet a year ago and ran a few CREATE and GRANT statements.
There is no history, no code review, no diff, and no alarm when one of them changes.
That is the gap Atlas fills. Atlas brings schema-as-code to Snowflake: you declare the desired state of the database in HCL or SQL, and it plans the changes, lints them, applies them, and checks afterwards that nothing has drifted. dbt keeps Silver and Gold; Atlas brings the underlying infrastructure to the same standard.
| Layer | What it holds | Owner | How it's managed |
|---|---|---|---|
| Platform | Schemas, roles, grants | Atlas | Roles and grants declaratively, schemas through planned migrations |
| Bronze | Landing tables, file formats | Atlas | Versioned migrations, with lint in review |
| Silver | Cleaned models | dbt | dbt materializations, which Atlas is configured to ignore |
| Gold | Business-ready models | dbt | The same |
| Cross-cutting | Migration history, linting, drift detection | Atlas | Runs across the layer both tools sit on |
Let's See It in Practice
Everything below runs against a Snowflake Standard-edition account, and is written up step by step in Snowflake and dbt: Managing the Schema Layer. It uses the versioned workflow with HCL as the schema format. The same layer can also be described in SQL or applied with the declarative workflow.
A Bronze Table is a Migration
The landing table and the file format that describes incoming files are declared in one file. The file format, for instance:
file_format "CSV_FORMAT" {
schema = schema.BRONZE
type = CSV
options = {
FIELD_DELIMITER = ","
SKIP_HEADER = 1
}
}
From that file, Atlas plans the DDL for the file format and the landing table together:
atlas migrate diff bronze_landing --env prod
-- Create file format "CSV_FORMAT"
CREATE FILE FORMAT "BRONZE"."CSV_FORMAT" TYPE = CSV SKIP_HEADER = 1 ESCAPE_UNENCLOSED_FIELD = '\\' NULL_IF = ('\\N');
-- Create "EVENTS" table
CREATE TABLE "BRONZE"."EVENTS" (
"EVENT_ID" NUMBER NOT NULL,
"USER_ID" NUMBER NOT NULL,
"EVENT_TYPE" VARCHAR(64) NOT NULL,
"OCCURRED_AT" TIMESTAMP_NTZ NOT NULL
) DATA_RETENTION_TIME_IN_DAYS = 1;
That file goes through review and atlas migrate lint like any other change. Lint reports
TX101 for it: the two statements cannot run in a single transaction, so a
failure between them would leave the database between versions.
dbt Maintains its Models
Two exclude patterns in atlas.hcl draw the line between the tools:
locals {
exclude = [
"SILVER.*",
"GOLD.*",
]
}
When dbt run builds the SILVER.STG_EVENTS table and the GOLD.DAILY_ACTIVE_USERS view, Atlas does
not see them, and comparing the account against the schema files finds nothing within its scope to
change:
atlas schema diff --env prod --from "env://url" --to "env://schema.src"
Schemas are synced, no changes to be made.
The schemas stay under Atlas because it creates them and holds their grants. What dbt builds inside them is dbt's, and neither tool plans changes to the other's objects.
Manual Changes to Bronze
Let's say someone adds a column directly in the account:
ALTER TABLE ANALYTICS.BRONZE.EVENTS ADD COLUMN SOURCE_SYSTEM VARCHAR(32);
The same command that stayed quiet about dbt's models reports this one:
atlas schema diff --env prod --from "env://url" --to "env://schema.src"
-- Modify "EVENTS" table to drop "SOURCE_SYSTEM" column
ALTER TABLE "BRONZE"."EVENTS" DROP COLUMN "SOURCE_SYSTEM";
Two Environments for Snowflake Objects
Snowflake nests most of what you create: an account holds databases, a database holds schemas, and a schema holds the tables and file formats above. Roles, warehouses, and users sit outside that nesting, on the account itself.
The difference shows up at planning time. Atlas plans by materializing the desired state in a dev database. In this setup, the dev database lives in the same account as the target. Anything inside a database gets a clean dev copy to plan against. An account-level object does not, because there is only one in the account.
Therefore, the project splits in two. Schemas and Bronze objects stay versioned for reviewed migration files and lint in CI, while roles and grants go declarative, where the file is the access model and Atlas diffs it against the account whenever you ask.
Drift as a Deployment Gate
atlas schema diff is the ad-hoc check. For deployment, use pre-apply drift detection:
a check "migrate_apply" block in atlas.hcl that runs at the start of every atlas migrate apply,
compares the target against the expected state for the latest applied revision, and aborts before
any migration file runs when they differ. The expected state comes from the
Atlas Registry, so the migration directory is pushed there with
atlas migrate push in CI. Start with on_error = CONTINUE to see existing drift without failing a
release, then switch to FAIL. This post's example stops short of the gate; the
warehouse drift guide covers its configuration, its registry
prerequisite, and its own exclude list.
Between deployments, schema monitoring runs the comparison on a
schedule. The
ariga/atlas-action/monitor/schema
GitHub Action syncs the schema on a cron with the same exclude patterns, so dbt's schemas stay out
of it. Without Atlas Cloud, atlas schema diff runs from any scheduler; it exits 0 whether or not it
finds drift, so the job itself has to decide what to do with the output.
Where Atlas Stops
Atlas and dbt draw a sharp line between platform and transformation.
Atlas manages the database structure, leaving table materializations, ref() lineages, and model execution entirely to dbt. It doesn't read dbt's manifest or trigger dbt build; your orchestrator still controls the DAG, and dbt still runs your transformations. Even testing remains cleanly divided. dbt tests validate the data inside your models, while Atlas tests validate the schema objects and migration safety underneath them.
The two tools don't overlap because they don't fight for ownership. dbt owns transformation logic; Atlas owns the platform it runs on.
Snowflake support, including roles and permissions, is available to Pro users and during a trial. The
driver ships in the extended build, so install it with ATLAS_FLAVOR=snowflake and run atlas login.
Wrapping Up
Snowflake and dbt: Managing the Schema Layer provides the full walkthrough: migrations and their output, roles and grants, the two environments Snowflake's object model calls for, the drift check, and what the example account could not exercise.
If you are choosing a Snowflake schema tool rather than integrating one with dbt, Atlas vs schemachange vs SnowDDL compares the options.