To create a migration in Laravel 13, run php artisan make:migration create_posts_table, define the schema in the up() method and the reverse in down(). Run pending migrations with php artisan migrate and undo the last batch with php artisan migrate:rollback. Use --step to roll back a specific number of migrations.
Migrations are version control for your schema. In team projects I treat them like code: every schema change is a migration, every migration is reviewed, and no one edits the production database by hand.
Prerequisites
- Laravel 13.x on PHP 8.3 or newer.
- Database credentials in
.env. New Laravel projects default to SQLite, which needs no server.
Step 1: Create a migration
php artisan make:migration create_posts_tableLaravel reads the name to guess the table. Names like create_posts_table generate a Schema::create() stub, and names like add_status_to_posts_table generate a Schema::table() stub. The file lands in database/migrations with a timestamp prefix that decides the run order.
You can also create a model and its migration together with php artisan make:model Post -m.
Step 2: Write the up and down methods
Laravel 13 uses anonymous migration classes, so file names never clash:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('title');
$table->string('slug')->unique();
$table->text('body');
$table->timestamp('published_at')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('posts');
}
};foreignId('user_id')->constrained() creates an unsigned big integer column and a foreign key to users.id, guessing the table from the column name.
Step 3: Run the migrations
php artisan migrateLaravel records every executed migration in the migrations table, together with a batch number. All migrations run by one migrate call share a batch. Useful companions:
# Which migrations have run, and in which batch?
php artisan migrate:status
# Show the SQL without executing it
php artisan migrate --pretend
# Give each migration its own batch, so you can roll back one at a time
php artisan migrate --stepHow do I roll back a migration in Laravel 13?
Rollback runs the down() methods in reverse order:
# Undo the last batch
php artisan migrate:rollback
# Undo the last 3 migrations, regardless of batch
php artisan migrate:rollback --step=3
# Undo one specific batch
php artisan migrate:rollback --batch=4
# Preview the rollback SQL
php artisan migrate:rollback --pretendWhat is the difference between migrate:reset, refresh and fresh?
migrate:resetrolls back every migration by calling alldown()methods.migrate:refreshresets and then runs everything again. Add--seedto run seeders afterwards.migrate:freshdrops all tables without callingdown(), then migrates. It is the fastest way to rebuild a local database, and also works when adown()method is broken.
All three destroy data. Use them locally or in tests, never on a production database you care about.
How do I add a column to an existing table?
Never edit a migration that has already run in another environment. Create a new one instead:
php artisan make:migration add_status_to_posts_table<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->string('status', 20)->default('draft')->after('slug')->index();
});
}
public function down(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->dropIndex(['status']);
$table->dropColumn('status');
});
}
};The after() modifier only affects MySQL and MariaDB column order. To change an existing column, call ->change() and repeat every modifier you want to keep, because missing attributes are dropped.
Running migrations in production
php artisan migrate --force --isolatedIn production Laravel asks for confirmation before destructive commands; --force skips the prompt for deploy scripts. --isolated takes an atomic lock through your cache driver, so two servers deploying at the same time do not run the same migrations twice. It needs a shared cache store such as Redis, database or Memcached.
When the migrations folder grows large, php artisan schema:dump --prune writes the current schema to database/schema and removes the old migration files. Laravel loads that dump first on a fresh database.
How should I name and organize migrations?
Descriptive names pay off months later when you scan migrate:status. I follow three habits: one logical change per migration, verbs that describe the change (create_, add_..._to_, drop_..._from_, rename_), and data fixes in their own migration, separate from schema changes, so a failed backfill does not block the structural change. Keep each migration small enough that its down() method is obvious. If two developers create migrations on different branches, the timestamps decide the order after merging, so avoid migrations that depend on another branch's unmerged table.
Common mistakes
- Editing old migrations. Colleagues and servers that already ran them will never see the change.
- Empty
down()methods. Rollbacks then succeed silently without undoing anything. - Dropping a column before its index or foreign key. Drop the constraint first, for example
$table->dropForeign(['user_id']), or usedropConstrainedForeignId('user_id'). - Using models inside migrations. Models change over time; old migrations then break. Use the query builder for data fixes.
- Running
migrate:freshagainst the wrong database. CheckDB_DATABASEbefore running it.
How to verify it works
Run php artisan migrate:status and confirm the new migration shows "Ran". Inspect the table with php artisan db:table posts, which lists columns, indexes and foreign keys. Then prove the migration is reversible: run php artisan migrate:rollback --step=1 followed by php artisan migrate. If both succeed, your down() method works.
FAQ
How do I run a single migration file?
Use the path option: php artisan migrate --path=database/migrations/2026_11_11_000000_create_posts_table.php.
Can I run migrations on another connection?
Yes. Pass --database=connection_name, or set a $connection property on the migration class.
Do tests need migrations?
Use the RefreshDatabase trait in your tests. It migrates the test database once and wraps each test in a transaction.



