Use when a team wants declarative, Terraform-style database schema management or migration linting beyond Flyway and Liquibase: Atlas schema apply, migrate diff against a dev database, migrate lint for destructive or locking changes, and migrate hash, across a dozen databases.
92%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
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). Two operating modes:
atlas schema apply).atlas migrate diff); migrations apply forward
(atlas migrate apply).Use versioned mode where every change needs a reviewed file and an audit trail; declarative mode suits fast local iteration.
Follow the steps in order. Lint and validate (Step 5) are the gate:
nothing reaches migrate apply (Step 6) until they pass.
Per at-start:
# 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). 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.
Per at-start, 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).
Per at-start:
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.
Per at-start:
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.
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).
Make the risky analyzers fail. Only destructive fails lint by
default (exit code 1); data-dependent and backward-incompatible
findings are only reported (analyzers). Turn them into
errors in atlas.hcl:
lint {
data_depend { error = true } # MF1xx: NOT NULL add, new unique index on existing data
incompatible { error = true } # BC1xx: rename / drop of tables and columns
}Run the gate:
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" --latest 1validate checks the directory against atlas.sum and, with
--dev-url, replays the files to check the SQL (CLI reference).
Fix and re-lint. For each finding, change the migration, not the
rule: add a DEFAULT or backfill before SET NOT NULL (MF103/MF104),
split a rename into add-copy-drop across releases (BC), build the
index CONCURRENTLY on PostgreSQL (PG1xx). Re-run
atlas migrate diff/atlas migrate hash after editing an unapplied
file, then repeat step 2 until it exits 0. Suppress a finding with
-- atlas:nolint <code> only with a comment saying why and a
reviewer's sign-off.
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). Atlas
records each applied version in the atlas_schema_revisions table, so
the next apply starts after the last recorded one.
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.
- 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).
ariga/setup-atlas puts the CLI on the runner (at-gha).
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.
Add a NOT NULL column status to a 40M-row users table:
schema.sql to add
status varchar(20) NOT NULL DEFAULT 'active' on users.atlas migrate diff add_status --to file://schema.sql --dev-url "docker://postgres/17/dev?search_path=public" writes
migrations/20260506120000_add_status.sql.atlas migrate validate and atlas migrate lint --latest 1 with the data_depend analyzer set to error. Without the
DEFAULT, MF103 (adding a non-nullable column) fails the lint; with
it, the gate passes.atlas migrate apply --dry-run then atlas migrate apply against
staging records the version in atlas_schema_revisions.atlas migrate apply fails on hash mismatch until an intentional
atlas migrate hash re-sync - the tamper guard working as
designed.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.
atlas commands, supported databasesariga/setup-atlas GitHub Actionmigrate lint flags (--latest, --git-base), Atlas Pro requirement from v0.38error = truemigrate apply --dry-run, migrate validateflyway-migrations, liquibase-migrations - sister tools
(imperative versioned migrations); migration-operation-taxonomy
and migration-blast-radius-reviewer for risk review