Difflock is a Laravel package from Rati Rukhadze for reviewing migrations against the database they will change. It reads migration source without loading or executing it, then uses the live schema and table-size metadata to flag changes that deserve a closer look before deployment.
Here is what Difflock includes:
- Migration linting for destructive operations, column changes, indexes, foreign keys, and sensitive column names
- Schema baselines recorded in JSON, which let you compare a database with a committed snapshot or another connection
- A migration guard that runs checks before handing control to Laravel's
migratecommand - CI checks with exit codes for risky migrations, schema drift, and configuration failures
- MCP tools for checking a table, a migration file, or migration source held in an agent's context
Reviewing Pending Migrations
difflock:lint analyses pending migrations by default. A migration such as this can look ordinary in a pull request:
Schema::table('orders', function (Blueprint $table) { $table->string('channel'); $table->foreignId('customer_id')->constrained()->cascadeOnDelete(); $table->string('card_number', 32)->nullable(); $table->index('status');}); Schema::table('customers', function (Blueprint $table) { $table->dropColumn('legacy_token'); $table->renameColumn('name', 'full_name');});
On a populated database, Difflock can flag the non-null channel column, the destructive dropColumn(), and the rename. It also reports that cascadeOnDelete() deletes child rows inside the database, which bypasses model events, observers, and soft deletes.
The package has rules for changes that static checks against an empty test database miss. change-column compares a ->change() call with the live column definition. unindexed-foreign-key checks whether the database engine adds the index a foreign key needs. redundant-index reports the certain leading-prefix case, such as adding an index on (status) when (status, created_at) already exists.
Use -v to expand the findings or --rule= to focus on one rule:
php artisan difflock:lintphp artisan difflock:lint -vphp artisan difflock:lint --rule=drop-column
When an application has no pending migrations, the command audits all migration files instead of producing an empty report. Existing projects can accept their current backlog with php artisan difflock:lint --all --accept, then use the resulting database/difflock/accepted.json file as the baseline for new findings.
Recording Schema Drift
Difflock records an observed schema rather than rebuilding an expected schema from migration files. That avoids trying to interpret migrations that contain conditionals, loops, or raw SQL.
php artisan difflock:diff --save
This writes database/difflock/schema.json, which you can commit. Later, php artisan difflock:diff compares the current connection against that file. You can also compare two configured connections:
php artisan difflock:diff --from=staging --to=production
The baseline contains schema structure, including tables, columns, indexes, defaults, and foreign keys. It does not include table rows or credentials. snapshot.defaults, snapshot.comments, and ignore.tables let you limit what is recorded. For another way to catch database issues in PostgreSQL, see Vacuum's schema linting.
CI and Migration Protection
Installing Difflock does not change php artisan migrate. The package only guards migrations when you use its own command:
php artisan difflock:migrate
It analyses pending migrations first. When findings reach the configured block level, it stops before Laravel writes to the database. --allow-risky deliberately bypasses Difflock's guard, while Laravel's --force remains the separate flag for production confirmation.
For CI, run the drift check and migration lint together:
- run: php artisan difflock:check --ci
The command exits with 0 when the check passes, 1 for drift or findings at the configured threshold, and 2 when the check cannot run. It can still analyse source-only rules without a database connection, but reports that table data was unavailable.
MCP Support
php artisan difflock:mcp starts a standalone MCP server over stdio. Its four tools provide table context, migration linting, schema-drift checks, and rule documentation.
The migration tool accepts either a file path or source text. An AI coding agent can check a migration before it writes the file, using the database's current schema and table statistics. The package also includes difflock:explain, which creates a Markdown briefing for a migration without calling a language model or any external API.
Installation
Difflock 1.0.0 requires PHP 8.3 and supports Laravel 12 and 13. It works with MySQL, MariaDB, PostgreSQL, and SQLite.
composer require heyosseus/difflock --devphp artisan vendor:publish --tag=difflock-configphp artisan difflock:doctor
difflock:doctor reports the connection, available tables, pending migrations, registered rules, and whether the configured database role can write. A read-only role is a sensible choice when you want Difflock to inspect a production connection.
Difflock is available on GitHub.