Every time your app evolves, your database needs to evolve with it. You add a feature that needs a new column, rename a table, or drop something you no longer need. Without a system to manage these changes, you end up with databases that are out of sync across your team and production. Migrations solve this by treating schemaWhat is schema?A formal definition of the structure your data must follow - which fields exist, what types they have, and which are required. changes as code.
The problem without migrations
Imagine you and a teammate are building the same app. You add a users table locally:
CREATE TABLE users (
id INTEGER PRIMARY KEY,
email TEXT
);Your teammate pulls your code and tries to run the app, but their database doesn't have the users table yet. They get errors. Meanwhile, in production you're copying SQLWhat is sql?A language for querying and managing data in relational databases, letting you insert, read, update, and delete rows across tables. from a notepad and hoping you don't make a typo. This is database chaos, and it happens to every team that skips migrations.
The shared-state problem
Your codebase is version-controlled with git, so every developer has the same source code. But the database schemaWhat is schema?A formal definition of the structure your data must follow - which fields exist, what types they have, and which are required. lives outside git, unless you explicitly put it in. Without migrations, you have no record of what SQL was run, in what order, or by whom.
Manual deployments are fragile
Running SQL by hand in production means relying on memory, copy-paste, and hoping nothing interrupts the process. A missed WHERE clause or a forgotten COMMIT can corrupt data with no easy path back.
How migrations work
A migrationWhat is migration?A versioned script that changes your database structure (add a column, create a table) so every developer and server stays in sync. is just a versioned SQLWhat is sql?A language for querying and managing data in relational databases, letting you insert, read, update, and delete rows across tables. file (or a JavaScript object with SQL inside). Your migration tool keeps a dedicated table that records which migrations have already been applied:
CREATE TABLE migrations (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);When you run npm run migrate, the tool compares your migration files against this table and runs only the ones that haven't been applied yet, always in order.
# You run:
npm run migrate
# The tool checks:
# Already ran: 001_create_users.sql
# Pending: 002_add_username.sql, 003_add_posts_table.sql
# Runs pending migrations in order, then records themUp and down scripts
Every migration has two halves. The "up" script applies the change; the "down" script reverses it. If a deployment goes wrong, you roll back cleanly instead of scrambling.
-- 002_add_username.sql
-- Up: Apply the change
ALTER TABLE users ADD COLUMN username TEXT;
-- Down: Undo the change
ALTER TABLE users DROP COLUMN username;What you gain
Reproducible environments
Every developer runs the same migrations and ends up with an identical schemaWhat is schema?A formal definition of the structure your data must follow - which fields exist, what types they have, and which are required.. No more "works on my machine" because the database structure is always in sync.
# Developer A
git pull && npm run migrate # Gets the latest schema
# Developer B
git pull && npm run migrate # Gets the exact same schema
# Production
git pull && npm run migrate # Same againA versioned schema history
Because migrationWhat is migration?A versioned script that changes your database structure (add a column, create a table) so every developer and server stays in sync. files live in git, you can trace every schema change back to the commitWhat is commit?A permanent snapshot of your staged changes saved in Git's history, identified by a unique hash and accompanied by a message describing what changed. that introduced it, along with the code change it was paired with.
git log migrations/
# commit abc123: Add posts table
# commit def456: Add username to users
# commit ghi789: Create users tableRollbackWhat is rollback?Undoing a database migration or deployment to restore the previous state when something goes wrong. capability
If a migration causes problems, you have a clear, tested path to undo it.
# Something went wrong after deploying
npm run migrate:rollback # Runs the "down" script for the last migrationThe migrationWhat is migration?A versioned script that changes your database structure (add a column, create a table) so every developer and server stays in sync. workflow
Here is what the day-to-day looks like once your project uses migrations:
# 1. Create a new migration file
npm run migrate:create add_avatar_to_users
# Creates: migrations/004_add_avatar_to_users.sql
# 2. Write the up/down SQL in that file
# 3. Apply it locally
npm run migrate
# 4. Test your app
# 5. Commit the migration with your code
git add migrations/004_add_avatar_to_users.sql
git commit -m "Add avatar column to users"When a teammate pulls your commitWhat is commit?A permanent snapshot of your staged changes saved in Git's history, identified by a unique hash and accompanied by a message describing what changed., they just run npm run migrate and they are caught up. The same command works for your staging environment and production.
Quick reference
| Command | What it does |
|---|---|
npm run migrate:create <name> | Create a new empty migration file |
npm run migrate | Run all pending migrations in order |
npm run migrate:rollback | Undo the last migration batch |
npm run migrate:status | Show which migrations have run and which are pending |
Common migrationWhat is migration?A versioned script that changes your database structure (add a column, create a table) so every developer and server stays in sync. patterns you will write over and over:
-- Add a column
-- Up
ALTER TABLE users ADD COLUMN avatar TEXT;
-- Down
ALTER TABLE users DROP COLUMN avatar;
-- Add a table
-- Up
CREATE TABLE posts (
id INTEGER PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
title TEXT,
content TEXT
);
-- Down
DROP TABLE posts;
-- Add an index
-- Up
CREATE INDEX idx_users_email ON users(email);
-- Down
DROP INDEX idx_users_email;