Foreign key columns must be unsigned — they can’t hold negative values, and MySQL enforces type parity between referenced and referencing columns. Laravel 12 gives you several ways to declare a column as unsigned. The right approach depends on how explicit you want to be and your specific use case.
[!TIP] Real-World Scenario: You’re building a SaaS platform with millions of records. Your
user_idcolumns need to reference theuserstable perfectly. One type mismatch and your migrations fail with cryptic SQL errors. Understanding unsigned columns saves you from production headaches.
$table->foreignId('user_id')is the recommended approach in Laravel 11+$table->unsignedBigInteger('user_id')is explicit and valid, but more verbose$table->integer('user_id')->unsigned()uses a modifier — works but deprecated in some driversforeignId()automatically creates an unsigned BIGINT and can chain->constrained()- All approaches work in Laravel 12
[!WARNING] Laravel 12 recommendation:
foreignId()is now the preferred method for foreign key columns. It automatically creates an unsigned BIGINT, matches the defaultid()column type, and chains directly into->constrained()for full foreign key constraint setup. Manually declaringunsignedBigInteger()is still valid but verbose by comparison.
How do I declare a foreign key column in Laravel 12?
Use foreignId(). It creates an unsigned BIGINT with one method and lets you attach the foreign key constraint in the same chain:
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained(); // references users.id
$table->foreignId('post_id')->constrained('posts'); // explicit table name
$table->timestamps();
});foreignId('user_id') infers the referenced table as users (strips the _id suffix). constrained() adds the actual FOREIGN KEY constraint. If you want unsigned without the constraint, just omit constrained():
$table->foreignId('user_id'); // unsigned BIGINT, no FK constraintThe scenario: You’re writing a migration for a
commentstable that belongs to users and posts. Instead of four lines per foreign key (declare column, mark unsigned, add index, add constraint),foreignId()->constrained()does it in one. That’s the practical reason it exists.
When should I use unsignedBigInteger() instead?
When you need explicit control over column definition and want to be unambiguous in a code review. It’s also useful in legacy codebases where foreignId() wasn’t available when the original migrations were written:
Schema::create('comments', function (Blueprint $table) {
$table->unsignedBigInteger('user_id');
$table->unsignedBigInteger('post_id');
});This is identical to what foreignId() creates under the hood — an unsigned BIGINT. It just doesn’t attach a foreign key constraint automatically. Add one explicitly if you need it:
$table->foreign('user_id')->references('id')->on('users');See Create Composite Indexes with Migration if you also need compound indexes on these columns.
What are alternative ways to create unsigned columns?
Laravel provides several unsigned column types and a modifier. These are all valid in Laravel 12:
Using unsignedInteger (and its variants)
$table->unsignedInteger('created_by');Use this when the referenced column is an INT, not a BIGINT. Mismatching signed/unsigned types between related columns causes a MySQL constraint error.
Using integer() with three parameters
The third parameter on integer() is the unsigned flag:
$table->integer('post_id', false, true); // (name, autoIncrement, unsigned)This is the lowest-level form. It works, but it’s harder to read at a glance. The false is autoIncrement and true is unsigned — easy to transpose.
Using the unsigned() modifier
$table->integer('reply_to_comment_id')->unsigned();The ->unsigned() modifier applied to integer() also works. It’s more readable than the three-parameter form, but still verbose compared to foreignId() for a foreign key column.
How do all approaches compare in one migration?
This migration shows all four approaches applied to the same table, with comments explaining when each makes sense:
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('comments', function (Blueprint $table) {
$table->id();
// Recommended: foreignId for FK columns in Laravel 11+
$table->foreignId('user_id')->constrained();
// Explicit: matches foreignId() output without the constraint
$table->unsignedBigInteger('post_id');
// Verbose: three-param form (avoid in new code)
$table->integer('approved_by', false, true);
// Modifier form: valid but wordy
$table->integer('reply_to_comment_id')->unsigned();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('comments');
}
};Note the anonymous class syntax (return new class extends Migration). This is the standard format in Laravel 9+ and is required in Laravel 12. Named migration classes still work but won’t be generated by php artisan make:migration.
Which unsigned column type should I choose?
| Column method | SQL type | Notes |
|---|---|---|
foreignId('col') | UNSIGNED BIGINT | Recommended for FK columns in Laravel 11+ |
unsignedBigInteger('col') | UNSIGNED BIGINT | Same type as foreignId(), no auto constraint |
unsignedInteger('col') | UNSIGNED INT | Use when referencing an INT primary key |
unsignedMediumInteger('col') | UNSIGNED MEDIUMINT | Smaller range, rarely needed |
unsignedSmallInteger('col') | UNSIGNED SMALLINT | Even smaller — for lookup tables with few rows |
unsignedTinyInteger('col') | UNSIGNED TINYINT | 0–255, good for status flags |
unsignedDecimal('col', 8, 2) | UNSIGNED DECIMAL | For non-negative monetary values |
For the full list, see the Laravel 12 Migration documentation.
Summary
- Use
foreignId()as the default for FK columns in Laravel 12 — it’s concise and self-documenting unsignedBigInteger()is the explicit alternative with identical output but more typing- Match unsigned types between referencing and referenced columns to avoid MySQL constraint errors
- Use anonymous class syntax (
return new class extends Migration) in all new migrations - Add
->constrained()to enforce foreign key constraints at the database level
FAQ
Does foreignId() add a foreign key constraint automatically?
No. foreignId('user_id') only creates the unsigned BIGINT column. You must chain ->constrained() to add the actual foreign key constraint that enforces referential integrity.
What happens if I use unsignedInteger() to reference an id() column?
You’ll get a MySQL error. Laravel’s id() creates a BIGINT. unsignedInteger() creates an INT. The types must match exactly — use unsignedBigInteger() or foreignId() instead.
Can I add a foreign key constraint to an existing column?
Yes. Use Schema::table() in a new migration:
Schema::table('comments', function (Blueprint $table) {
$table->foreign('user_id')->references('id')->on('users');
});Should I use unsigned for non-foreign key columns?
Use unsigned when values will never be negative — likes_count, view_count, position, or inventory_quantity. This adds a database-level constraint preventing negative values.
What is the maximum value for unsignedBigInteger? Unsigned BIGINT ranges from 0 to 18,446,744,073,709,551,615. You’ll exhaust disk space before exhausting this range.
What to Read Next
- Create Composite Indexes with Migration in Laravel — add performance indexes alongside your foreign keys
- Create Indexes with Migration in Laravel — optimize query performance on foreign key columns
- Install and Configure PHP on Ubuntu 26.04 — set up your Laravel development environment



