If you’ve ever had to manually compare SQL migration files between staging and production to figure out “what’s missing,” you know exactly how painful that is. I’ve been through this more times than I’d like — especially on PostgreSQL projects with complex schemas, where the whole team was committing migrations without any proper tooling to manage them.
Atlas by Ariga was built to solve exactly that problem: manage your database schema like code (Schema as Code / IaC) — versioned, automatable, and consistent across every environment.
Three Approaches to Schema Management: A Practical Comparison
To understand what makes Atlas different, let’s quickly walk through the three most common approaches:
1. Manual migration files (SQL/ORM)
This is the most common approach — using Flyway, Liquibase, or ORM migrations (Alembic, Django migrations, Rails ActiveRecord). You write SQL files in order, commit them to git, and run migrate on deploy.
- Pros: Familiar, well-documented, natively integrated with most ORMs
- Cons: Migration files pile up over time, auditing the current schema is hard, conflicts are frequent when multiple people work on it simultaneously, and there’s no automatic drift detection between code and the actual database
2. Declarative schema (describe the desired state)
Instead of writing “what to do” (ALTER TABLE…), you write “what you want” — what the final schema should look like. The tool computes the diff and generates the migration automatically. This is how Terraform works for infrastructure; Atlas applies the same philosophy to databases.
- Pros: Schema is always the source of truth, easy to read and review, no drift
- Cons: Requires learning the tool; some edge cases need manual intervention
3. Atlas: A hybrid approach — the best of both worlds
Atlas supports both modes: declarative (schema.sql/HCL describes the desired end state) and versioned migration (generates auditable .sql files). You can choose whichever fits your workflow, or combine both depending on the project stage.
Deep Dive: Atlas vs Flyway/Liquibase
| Criteria | Flyway / Liquibase | Atlas |
|---|---|---|
| Schema drift detection | Not available | Yes (atlas schema diff) |
| Declarative mode | No | Yes |
| CI/CD integration | Moderate | Strong (native GitHub Actions) |
| Migration linting | Not available | Yes (atlas migrate lint) |
| Multi-database support | Yes (many) | PG, MySQL, SQLite, MS SQL |
| Schema format | SQL / XML / YAML | HCL + SQL |
My favorite feature in Atlas is atlas migrate lint — it analyzes migrations before applying them and warns about destructive operations (DROP TABLE, DROP COLUMN) or missing indexes on foreign keys. Flyway doesn’t have this, and I once accidentally dropped a column on staging because nobody reviewed it carefully enough.
When Should You Choose Atlas?
After using Atlas on several PostgreSQL production projects, I’ve found it fits best when:
- A team of 3+ people is modifying the schema and needs to review and approve migrations
- You have multiple environments (dev/staging/prod) and need schema consistency across all of them
- You want to integrate migration checks into your CI/CD pipeline to block merges when issues are detected
- Your project uses PostgreSQL or MySQL with a reasonably complex schema
On the other hand, if you’re working on a small solo project with Django or Rails, your framework’s ORM migrations are perfectly adequate — don’t over-engineer it.
Installing Atlas
Atlas is a single binary with no runtime dependencies:
# macOS
brew install ariga/tap/atlas
# Linux (one-liner)
curl -sSf https://atlasgo.sh | sh
# Check version
atlas version
Real-World Setup: Versioned Migrations with PostgreSQL
The workflow below is what I’m currently running in production. All examples use PostgreSQL 15.
Step 1: Create the project structure
mkdir myapp-db && cd myapp-db
mkdir -p migrations
Step 2: Create schema.sql — the source of truth
-- schema.sql: The "desired state" of the database
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
username VARCHAR(100) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title VARCHAR(500) NOT NULL,
body TEXT,
published BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_published ON posts(published) WHERE published = TRUE;
Step 3: Generate the first migration file
atlas migrate diff initial_schema \
--dir "file://migrations" \
--to "file://schema.sql" \
--dev-url "docker://postgres/15/dev"
The --dev-url "docker://postgres/15/dev" flag is quite handy — Atlas automatically spins up a temporary PostgreSQL container to compute the diff, so you don’t need Postgres installed locally. After running, you’ll have:
migrations/
20260521000001_initial_schema.sql
atlas.sum # checksum file — commit this alongside migrations
The atlas.sum file is important: Atlas uses it to detect if anyone modifies an already-committed migration (similar to go.sum). Always commit this file to git.
Step 4: Apply the migration to your actual database
atlas migrate apply \
--dir "file://migrations" \
--url "postgres://user:pass@localhost:5432/myapp?sslmode=disable"
# Output:
# Migrating to version 20260521000001 (1 migration in total):
# -- migrating version 20260521000001
# -> CREATE TABLE users ...
# -> CREATE TABLE posts ...
# -- ok (23ms)
Step 5: Adding a new column — the schema change workflow
Product requests a new bio column on the users table. Instead of writing ALTER TABLE by hand, just edit schema.sql:
-- Add to the users table definition:
bio TEXT,
Then run migrate diff again:
atlas migrate diff add_user_bio \
--dir "file://migrations" \
--to "file://schema.sql" \
--dev-url "docker://postgres/15/dev"
# Atlas automatically generates:
# ALTER TABLE "users" ADD COLUMN "bio" text;
Battle-Tested Tips
1. Run migrate lint before merging a PR
atlas migrate lint \
--dir "file://migrations" \
--dev-url "docker://postgres/15/dev" \
--latest 1
# Atlas warns about destructive changes:
# L1: Dropping non-virtual column "email" (MF103)
# L2: Adding a non-nullable column "phone" to existing table (MF103)
Integrate this into GitHub Actions to block merges when destructive changes haven’t been properly reviewed:
# .github/workflows/atlas-lint.yml
name: Atlas Migration Lint
on:
pull_request:
paths: ['migrations/**', 'schema.sql']
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ariga/setup-atlas@v0
- name: Lint migrations
uses: ariga/atlas-action/migrate/lint@v1
with:
dir: 'file://migrations'
dev-url: 'docker://postgres/15/dev'
2. Detect drift between schema code and the real database
# Compare the current DB schema against schema.sql
atlas schema diff \
--from "postgres://user:pass@prod-host:5432/myapp?sslmode=require" \
--to "file://schema.sql"
# If drift exists, the output clearly shows where the DB has diverged from code
Flyway doesn’t have this feature. I run this periodically in CI to catch cases where someone ALTER TABLEd directly on production — which happens more often than you’d think, especially during a 2am hotfix.
3. Use atlas.hcl to clean up your commands
# atlas.hcl
env "dev" {
src = "file://schema.sql"
url = "postgres://user:pass@localhost:5432/myapp?sslmode=disable"
dev = "docker://postgres/15/dev"
migration {
dir = "file://migrations"
}
}
env "prod" {
url = getenv("DATABASE_URL")
migration {
dir = "file://migrations"
}
}
With this file, commands become much shorter:
atlas migrate apply --env dev
atlas migrate apply --env prod
Common Errors and How to Fix Them
- Checksum mismatch: Someone edited an already-committed migration file. Confirm the change is intentional, then run
atlas migrate hash --forceto regenerate the checksum. - docker:// not working: Docker needs to be running and Atlas needs socket access. Replace with a real dev database URL if Docker isn’t available.
- Migration stuck due to advisory lock: If a previous process was killed mid-run, execute
SELECT pg_advisory_unlock_all();on PostgreSQL to release the lock.
Conclusion
Atlas isn’t the only database migration tool out there. But drift detection, destructive change linting, and CI/CD integration are things Flyway and Liquibase simply don’t do. I’ve migrated one PostgreSQL project from Alembic to Atlas — and I haven’t had a reason to go back, especially when the whole team needs to review migrations before merging.
If you’re setting up a new project, give Atlas a try from day one — 30 minutes of upfront setup will save you a lot of headaches later as your schema grows in complexity.

