Overview
Atlas manages and migrates database schemas as code - define the desired schema and Atlas plans, lints, tests, and applies the changes (per atlasgo.io/getting-started (opens in new window)). Two operating modes:
Use versioned mode where every change needs a reviewed file and an audit trail; declarative mode suits fast local iteration.
How to use
Follow the steps in order. Lint and validate (Step 5) are the gate: nothing reaches migrate apply (Step 6) until they pass.
Step 1 - Install
Per at-start (opens in new window):
# Homebrew (macOS / Linux)
brew install ariga/tap/atlas
# Docker: nothing to install, mount the migrations directory
docker run --rm --net=host -v "$(pwd)/migrations:/migrations" \
arigaio/atlas migrate apply --url "$DATABASE_URL"In GitHub Actions, uses: ariga/setup-atlas@v0 puts the CLI on the runner (Step 8, per at-gha (opens in new window)). The getting-started page also lists per-platform binaries with md5 and sha256 checksums; verify the checksum before running a downloaded binary. Prefer these over piping the vendor install script into a shell, which runs unreviewed remote code.
Step 2 - Define schema
Per at-start (opens in new window), schema can be SQL (most common) or HCL. SQL example (PostgreSQL):
-- schema.sql
CREATE TABLE users (
id serial PRIMARY KEY,
name varchar(255) NOT NULL,
email varchar(255) UNIQUE NOT NULL,
created_at timestamptz DEFAULT now()
);For the same schema expressed in Atlas's HCL DSL and guidance on when HCL vs SQL matters, see references/hcl-schema.md.
The dev DB referenced via --dev-url is a temporary scratch database Atlas uses to compute the diff (Atlas applies the desired state to the scratch DB, then computes the migration vs production).
Step 3 - Apply schema (declarative mode)
Per at-start (opens in new window):
atlas schema apply --url "$DATABASE_URL" --to file://schema.sql \
--dev-url "docker://postgres/17/dev?search_path=public"--url = target DB; --to = desired schema definition; --dev-url = scratch DB for diff computation. Atlas prints the planned changes and prompts for confirmation by default.
Step 4 - Generate versioned migrations
Per at-start (opens in new window):
atlas migrate diff initial --to file://schema.sql \
--dev-url "docker://postgres/17/dev?search_path=public"This generates a timestamped migration file in migrations/ (e.g., 20260506120000_initial.sql). Subsequent calls with a new schema generate new migrations against the previous state.
Step 5 - Gate: validate and lint before any apply
atlas migrate lint needs Atlas Pro from v0.38 (atlas login first); without it, keep migrate validate and use the migration-blast-radius-reviewer agent for the risk review (lint docs (opens in new window)).
Step 6 - Apply versioned migrations
atlas migrate apply --url "$DATABASE_URL" --dry-run # print the SQL
atlas migrate apply --url "$DATABASE_URL"
Read the dry-run SQL before the real run (CLI reference (opens in new window)). Atlas records each applied version in the atlas_schema_revisions table, so the next apply starts after the last recorded one.
Step 7 - Hash and integrity
atlas.sum holds a hash per migration file. Editing a file changes the hash and migrate validate / migrate apply fail until atlas migrate hash is run, which must be a deliberate act: never edit a migration that has already been applied anywhere; add a new one.
Step 8 - CI integration
- uses: ariga/setup-atlas@v0
- name: Validate and lint (gate)
run: |
atlas migrate validate --dir "file://migrations" \
--dev-url "docker://postgres/17/dev?search_path=public"
atlas migrate lint --dir "file://migrations" \
--dev-url "docker://postgres/17/dev?search_path=public" \
--git-base origin/main
- name: Apply migrations to staging
if: github.ref == 'refs/heads/main'
run: atlas migrate apply --url "${{ secrets.STAGING_DB_URL }}"
--git-base lints every migration the branch adds compared with the base branch, instead of a fixed --latest N (lint docs (opens in new window)). ariga/setup-atlas puts the CLI on the runner (at-gha (opens in new window)).
Step 9 - Composition with sister tools
Atlas lint knows SQL patterns, not your data volumes or release order. Run the migration-blast-radius-reviewer agent on the PR for team policies such as "no DROP TABLE without DBA approval", and use migration-operation-taxonomy to classify each operation's lock and rollback risk.
Worked example
Add a NOT NULL column status to a 40M-row users table:
Anti-patterns and limitations
The failure modes (editing applied migrations, skipping --dev-url, declarative schema apply straight to production, skipping lint in CI, blind atlas migrate hash) and the tool's limitations (partial HCL for some DBMS features, general-purpose lint rules, --dev-url requiring a real DBMS, paid Atlas Cloud) are catalogued in references/atlas-caveats.md.
References
Atlas migration anti-patterns and limitations
View source (opens in a new tab) (opens in new window)Reference catalogue for the atlas-migrations skill. Each anti-pattern maps to a fix; each limitation names a fallback.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Edit a migration after it's been applied to staging/prod | atlas migrate apply fails on hash mismatch | Add a new migration that adjusts |
Skip --dev-url | Atlas can't compute diff; commands fail or produce wrong output | Always pass --dev-url (Steps 3, 4, 6) |
Use declarative schema apply directly to production | No audit trail of applied changes | Use versioned mode in production (Step 4) |
Skip atlas migrate lint in CI | Destructive migrations slip through review | Always lint in CI (Step 8) |
atlas migrate hash after every edit (without team review) | Defeats integrity check | Hash sync only after intentional edit + team review |
Limitations
Atlas accepts the desired schema as SQL (most common) or as HCL, Atlas's own schema DSL. Both are desired-state inputs to the same atlas schema apply and atlas migrate diff commands (Steps 3-4), passed via --to file://<schema-file>.
HCL example
The users table from Step 2's SQL example, expressed in HCL:
table "users" {
schema = schema.public
column "id" {
null = false
type = serial
}
column "email" {
null = false
type = varchar(255)
}
primary_key { columns = [column.id] }
index "unique_email" { unique = true; columns = [column.email] }
}
HCL vs SQL - which to author
Either representation produces the same versioned migrations once diffed against the --dev-url scratch DB.