Skip to main content

Medallion Architecture on Snowflake with Atlas and dbt

· 7 min read
Nhan Nguyen
Analytics Engineer

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.

LayerWhat it holdsOwnerHow it's managed
PlatformSchemas, roles, grantsAtlasRoles and grants declaratively, schemas through planned migrations
BronzeLanding tables, file formatsAtlasVersioned migrations, with lint in review
SilverCleaned modelsdbtdbt materializations, which Atlas is configured to ignore
GoldBusiness-ready modelsdbtThe same
Cross-cuttingMigration history, linting, drift detectionAtlasRuns across the layer both tools sit on

Bronze, silver and gold sit on a platform layer of schemas, roles and grants that Atlas manages as code. Atlas also owns bronze; dbt builds silver from bronze and gold from silver.

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:

schema.layer.hcl
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
migrations/20261008032251_bronze_landing.sql
-- 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:

atlas.hcl
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.