Skip to main content

Installation

Requirements​

ComponentConstraint
PHP^8.4 (8.4 and 8.5)
Laravel^13.0
Laravel Fortify^1.39 — optional, only for the two-factor handoff
DatabasePostgreSQL 10+, Laravel's own floor, or MySQL 8.0.1+ — the purge claims rows with FOR UPDATE SKIP LOCKED, which older MySQL servers reject. Tested on PostgreSQL 18 and MySQL 8.4

The package requires laravel/framework (for the FormRequest base it validates with), and beyond it only nesbot/carbon and symfony/http-foundation, both of which every Laravel application already installs. Fortify is a suggested dependency: the core never references a Fortify symbol unless Fortify is installed and the bridge is enabled.

Install the package​

composer require pushery/email-magic-link-for-laravel

Then run the installer to publish the configuration and print the next steps:

php artisan email-magic-link:install

Now run your migrations to create the token table. The migration ships with the package and is loaded automatically — you do not need to publish it first:

php artisan migrate

Add --views to the installer to also publish the Blade views. Prefer to do it by hand? The individual publish tags are still available:

php artisan vendor:publish --tag=email-magic-link # everything at once
php artisan vendor:publish --tag=email-magic-link-config
php artisan vendor:publish --tag=email-magic-link-migrations
php artisan vendor:publish --tag=email-magic-link-views
php artisan vendor:publish --tag=email-magic-link-lang

Publish the migrations only if you need to change them. The package loads its bundled migrations itself, and the published copies get a fresh date prefix, so both would be pending for the same tables; once a published *_create_magic_link_tokens_table.php exists the package stops loading its own. If you rename that file, say so explicitly by calling EmailMagicLinkServiceProvider::ignoreMigrations(tablesExist: true) in a service provider's register(). The argument tells the package the tables exist, so a scheduled purge keeps running; a bare ignoreMigrations() means you declined them, and the purge is not scheduled.

The command's full option list is in the command reference.

Run a queue worker​

The magic-link email is dispatched to the queue. This is deliberate: the request that issues a link never waits for the mailer, so the time a mail takes to deliver, which is only spent for an address that has an account, never shows in the response. The trade-off is that the mail is only actually sent once a queue worker processes the job, so a worker has to be running:

php artisan queue:work

If nothing is consuming the queue, the email never leaves — the request still succeeds, but the job just sits there. In production, keep a worker (or Horizon or Supervisor) running. For local development you can send mail synchronously instead by setting QUEUE_CONNECTION=sync in your .env.

Schedule the purge​

Every request writes a token row, and consumption only marks it spent, so the table needs a regular purge. Switch on the package's own schedule:

// config/email-magic-link.php
'prune' => ['schedule' => true],

It is off by default, so the package never deletes rows on a cadence you did not choose. If you keep your scheduler entries in your own file, schedule email-magic-link:purge there instead and leave the switch off. Keeping the token table small has both ways and the cadence.

Next​