Skip to content

Migrations ​

Each module has its own database, so migrate runs once per module database. It never runs once for "the application": there is no database that belongs to no module.

What runs in a module's database ​

Four sources feed one module's database:

SourceWhere it livesIn the starterWhy it is in every module's database
The applicationdatabase/migrationsfailed_jobs and job_batchesa failed job or a batch is stored in the database of the module that owns the job class, so each module database needs the table
The packagesthe paths a package registersthe package: event_consumptions, unless the guard is off; event_publications and its version column, only when DISTRIBUTABLE_EVENTS_RELIABLE_PUBLISH is onthe publication row is written in the emitting module's database, in its transaction; the consumption mark is written in the consuming module's database
The module{module}/database/migrationsiam: its users and its permission tablesthe module's own tables; no other module may name them
The copiesthe files or folders the copy's migrations() namesiam_users, copied into analytics and notificationsthe owner publishes the migration, each module that keeps the copy runs it

Laravel sorts every migration of a run by file name, whatever its source. The table lists where they come from, not the order they run in.

The starter, database by database ​

DatabaseApplicationPackageModuleCopyFiles
iam10203
analytics10315
notifications10113

The package column counts the package's own tables only, with the starter's settings: events not published reliably and the guard off, so no table. DISTRIBUTABLE_EVENTS_RELIABLE_PUBLISH=true adds 2 (event_publications and its version column), and removing DISTRIBUTABLE_STREAM_GUARD=false adds event_consumptions. The copy exists in analytics and notifications because their providers list UserShadow in $shadows; iam owns the table and keeps no copy.

Running them ​

bash
# every module database, one pass each
php artisan migrate

# the databases of these modules only; the option can be repeated
php artisan migrate --module=analytics

# what ran, and what is pending, in each database
php artisan migrate:status

migrate:rollback, migrate:reset, migrate:refresh and migrate:fresh follow the same rule. --seed seeds each module database with that module's own seeders.

RuleDetail
The connectionthe connection the module's owner names, or its default without one (see Its database)
Historyeach database keeps its own migrations table
--database or --paththe command is Laravel's own, unchanged
TestsRefreshDatabase follows the same passes

Writing a migration ​

A migration of the module's own tables goes in the module:

bash
php artisan make:migration add_archived_at_to_notifications_inbox --module=notifications

It lands in apps/Notifications/database/migrations.

The migration of a copy is published by the module that owns the table. The copy names where its migrations are in #[Shadow(migrations: …)], next to the module: and the table:: files or folders, as Laravel's loadMigrationsFrom(). A folder brings every migration in it, whatever its file name.

php
// foundation/Iam/Shadows/UserShadow.php
#[Shadow(module: 'iam', table: 'iam_users', migrations: [__DIR__.'/../database/shadows/users'])]
class UserShadow extends ShadowModel
{
}

The migration is a plain Laravel migration of the copy's table:

php
// foundation/Iam/database/shadows/users/2026_01_01_000100_create_iam_users_shadow_table.php
return new class extends Migration
{
    public function up(): void
    {
        Schema::create((new UserShadow)->getTable(), function (Blueprint $table): void {
            $table->unsignedBigInteger('id')->primary();
            $table->string('name');
            $table->string('email');
            $table->timestamp('created_at')->nullable();
            $table->timestamp('updated_at')->nullable();
            $table->timestamp('deleted_at')->nullable();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists((new UserShadow)->getTable());
    }
};

The source table is written once, in the copy's sourceTable(): renaming it moves neither the folder nor the migration. A module whose subclass of the copy sets its own $table also overrides migrations(), and names a migration of its own database/migrations. modules:doctor reports a path a kept copy names that holds no migration.

Limits ​

  • A module without a database has no migrations. modules:doctor reports a module that has some.
  • The application's database/migrations run in every module database. Keep that folder to what every module needs.
  • A module cannot name a table that another module's migrations create. Boundaries reports it. The table of a copy the module keeps is its own.
  • The database drivers of the cache, the queue and the session need a connection naming a module's: outside a module, the default connection is no_module.