Difflock checks Laravel migrations before production
A Laravel linter that evaluates migrations against the real database schema before they reach production.
A migration can work and still be unsafe
A migration can pass code review, work against an empty test database, and still be the wrong move in production. Adding a required column without a default, dropping a column, or building an index on a populated table are not neutral operations. The migration source alone does not provide enough context: the current schema and the size of the affected table matter too. (github.com)
Difflock is a Laravel package built around that missing context. It statically reads pending migrations and inspects the configured database schema to identify changes that need a closer look. I do not see it as a tool that makes deployment decisions for me; I see it as a guard that prevents destructive or risky changes from slipping through unnoticed. (github.com)
What it looks for
The package covers cases such as dropped tables and columns, renamed columns, change() calls, indexes, and foreign keys. Some findings depend on the table row count, so identical migration code can be assessed differently on an empty table and on a populated one. It can also compare the live schema with a recorded baseline to catch schema drift, including changes made outside the migration history. (github.com)
The initial workflow is simple enough. I would save a baseline for an approved schema, lint pending migrations, and then run both checks in CI. Difflock requires PHP 8.3 or newer and targets Laravel 12 and Laravel 13. (github.com)
composer require heyosseus/difflock --dev
php artisan difflock:diff --save
php artisan difflock:lint
php artisan difflock:check --cidifflock:diff can compare the live schema with a baseline or compare two named connections through --from and --to. difflock:lint checks pending migrations by default, while difflock:check --ci combines schema drift and migration analysis with CI-friendly exit codes. For a full historical audit there is difflock:lint --all, although I would not turn that into an immediate blocker in an older application without dealing with the existing backlog first. (github.com)
A guard you must adopt deliberately
The useful part is not limited to reporting. php artisan difflock:migrate analyses pending migrations first and only hands control to Laravel’s own migrator when the configured protection.block_on threshold is not reached. If I have reviewed a finding and still want to continue, --allow-risky makes that decision explicit instead of hiding it inside a deployment script. (github.com)
The published configuration lets me choose the connection, baseline path, and risk thresholds. I would start with a conservative policy, then adjust limits only when there is a specific reason tied to the application’s workload or release process. I would also treat ignore rules carefully: they remove findings, but they do not make a questionable migration safer. (github.com)
// config/difflock.php
return [
'baseline' => env('DIFFLOCK_BASELINE', database_path('difflock/schema.json')),
'risk' => [
'fail_on' => env('DIFFLOCK_FAIL_ON', 'critical'),
],
'protection' => [
'enabled' => env('DIFFLOCK_PROTECTION_ENABLED', true),
'block_on' => env('DIFFLOCK_BLOCK_ON', 'critical'),
],
];The limitations are part of the value
Difflock does not replace a migration strategy designed for staged releases. It cannot promise lock-free changes or zero downtime, because those outcomes depend on the database engine, version, configuration, and sometimes the actual data. It also has no knowledge of my application’s query workload, so it can tell me an index is being removed but not whether a particular query will become slower. (github.com)
There is another important boundary: migrations are executable PHP, but Difflock analyses them without running them. That means operations inside an if may be marked as conditional, dynamically built names may remain unresolved, and raw SQL through DB::statement() produces a warning rather than false confidence. I prefer that approach to a tool pretending it understands code it cannot safely interpret. (github.com)
It also does not hook itself into php artisan migrate automatically. I need to use difflock:migrate or add difflock:check --ci to the delivery pipeline if I want it to become an actual gate. SQLite is worth calling out too: it exposes less metadata than MySQL, MariaDB, or PostgreSQL, so some details such as lengths and precisions cannot be compared there. (github.com)
Where I would use it
I would start by adding php artisan difflock:check --ci to CI rather than turning a legacy codebase into a wall of failing builds overnight. Then I would generate and commit a baseline, review the old findings, and accept only the known backlog the team has consciously decided to carry. From that point on, every new migration would have to expose its schema risk before reaching production. (github.com)
For Laravel applications with tables that already hold meaningful data, this is a useful layer between migration code and the real database. It does not replace engineering judgement, backups, or a deployment plan, but it makes risk visible while it is still cheap to address. That is more valuable than another command that merely confirms the syntax looks valid.
Source: Laravel News Official documentation: Difflock on GitHub