Skip to content
Go back

How Laravel Migrations Work (And Why You Don't Touch the Database Directly)

Migrations were the scariest part of Laravel for me. The word alone sounds like something that could break your entire database. When Claude Code told me it had “created a migration file,” my first question was:

“can you explain this in more detail please, I don’t understand? do I need to manually do/update something?”

The answer was reassuring — and once I understood what migrations actually are, they stopped being scary and became one of my favourite things about Laravel.

Table of contents

Open Table of contents

What is a migration?

A migration is a PHP file that describes a change to your database. Instead of logging into your database and manually changing columns, defaults, or table structures, you write a migration file that does it for you.

When Claude Code explained it to me, this is what clicked:

“A migration is just how Laravel manages database changes. Instead of going into the database directly and changing things, you create a migration file (a PHP file) that describes what to change. Then you run php artisan migrate and Laravel applies it.”

Think of it like a recipe. The migration file says “change the campaign duration default from 42 to 28 days.” You run the command, and Laravel follows the recipe. The file itself lives in your codebase alongside your other code, which means it gets committed to git, reviewed, and deployed just like everything else.

Why not just change the database directly?

This was my instinct — just open the database and change the value. But there are good reasons not to:

ApproachWhat happensRisk
Change database directlyFixed on your machine only. No record of what changed. Production still has the old value.You forget what you changed. Someone else has a different database state. Production and local get out of sync.
Use a migrationChange is written as code. Gets committed, reviewed, and deployed. Everyone’s database updates automatically.Very low — migrations are reversible and run in order.

The Laravel documentation describes migrations as “like version control for your database.” That’s exactly right. Just as git tracks changes to your code, migrations track changes to your database.

A real example: changing the campaign duration default

My app has a settings page where you can see the default campaign duration. It was set to 42 days and wasn’t editable. I wanted to change the default to 28 days and let users edit it.

I told Claude Code:

“I’d like to change the default campaign duration to be 28 days (not 42), I’d also like for the field to be editable”

This required three changes: making the settings field editable (a UI change), adding validation (a code change), and updating the database default (a migration).

The migration file

Here’s the actual migration that was created:

return new class extends Migration
{
    public function up(): void
    {
        // Update existing teams that have the old default of 42
        DB::table('teams')->where('experiment_duration_target', 42)->update([
            'experiment_duration_target' => 28,
        ]);

        // Change the column default to 28
        Schema::table('teams', function (Blueprint $table) {
            $table->integer('experiment_duration_target')
                  ->nullable()
                  ->default(28)
                  ->change();
        });
    }

    public function down(): void
    {
        DB::table('teams')->where('experiment_duration_target', 28)->update([
            'experiment_duration_target' => 42,
        ]);

        Schema::table('teams', function (Blueprint $table) {
            $table->integer('experiment_duration_target')
                  ->nullable()
                  ->default(42)
                  ->change();
        });
    }
};

Two things to notice:

  1. up() runs the change forward — updates existing teams from 42 to 28, and sets the new default.
  2. down() reverses it — if something goes wrong, Laravel can undo the migration and restore the old values.

This is what makes migrations safe. Every change has a built-in undo button.

The UI change

The settings field went from read-only to editable:

<!-- Before: read-only display -->
<flux:input :value="$team['experiment_duration_target']"
  readonly icon:trailing="calendar-days" suffix="days" />

<!-- After: editable number field -->
<flux:input wire:model="team.experiment_duration_target"
  type="number" min="1" icon:trailing="calendar-days" suffix="days" />

And validation was added to the Livewire controller to ensure the value stays between 1 and 365 days:

'team.experiment_duration_target' => 'required|integer|min:1|max:365',

But do I need to run this on the live database?

This was my next worry:

“do I need to run that script on the live environment and live database? how do I do that?”

I use Laravel Forge for deployment, and it turns out Forge handles this automatically:

“Laravel Forge runs php artisan migrate --force automatically on every deploy by default. So you don’t need to do anything manually — when you push the code with the new migration file, Forge will run it automatically when it deploys.”

So the workflow is: make the changes locally, commit, push to GitHub, and Forge deploys and runs the migration. No manual database work. No logging into a production server.

Always back up your database first

Migrations are safe by design, but they’re still changing your production database. Things can go wrong — a migration might time out on a large table, or a data update might not work as expected.

I’ve since set up a custom skill in Claude Code called commit-check that runs before every commit. Part of what it does:

“Where alternative approaches exist, present pros and cons along with your recommendation. Double-check any database migrations and always suggest running a database backup first.”

This means every time I commit code that includes a migration, I get a reminder to back up the database before deploying. It’s a small safety net that costs nothing but could save you from a very bad day.

If you’re using Forge, you can set up automatic database backups under your server’s Backups tab. For manual backups, a quick mysqldump before deploying migration-heavy changes is good practice.

Some migrations are intentionally irreversible

Not every migration can be undone. I ran into this when renaming campaign stages — merging “Unscored” and “Scored” into a single “Prioritised” stage:

public function up(): void
{
    DB::table('ideas')
        ->whereIn('stage', ['Unscored', 'Scored'])
        ->update(['stage' => 'Prioritised']);
}

public function down(): void
{
    // Intentionally irreversible — we can't distinguish which
    // "Prioritised" rows were originally "Unscored" vs "Scored"
}

The down() method is empty on purpose. Once you merge two values into one, the information about which was which is lost. You can’t reverse it.

I asked:

“double check the database migration and explain it again”

This is exactly why backups matter. Before running an irreversible migration, take a database backup. If something goes wrong, the backup is your only way back.

One other detail: use DB::table() instead of Eloquent models in migrations. Eloquent models have scopes, casts, and accessors that can silently filter out records. DB::table() works directly with the raw data — no surprises.

The migration workflow at a glance

Here’s the full lifecycle of a database change in Laravel:

  1. You describe the change — “change the default from 42 to 28 days”
  2. A migration file is created — a PHP file in database/migrations/ with a timestamp in the filename
  3. You run it locallyphp artisan migrate applies the change to your local database
  4. You commit and push — the migration file goes into git like any other code change
  5. Forge deploys and migrates — your production database updates automatically

The migration file is the source of truth. Anyone who pulls the code and runs php artisan migrate gets the same database state. No more “it works on my machine” problems.

Key takeaway

Migrations are not scary — they’re Laravel’s way of making database changes safe, reversible, and trackable. Instead of manually editing your database (and hoping you remember what you changed), you write a file that describes the change, and Laravel handles the rest. If you’re using Forge, the whole process is automatic: push your code, and the database updates on deploy.


Back to top ↑