Installation
Requirements
| Component | Constraint |
|---|---|
| PHP | ^8.4 (8.4 and 8.5) |
| Laravel | ^13.0 |
| Laravel Fortify | ^1.39 — optional, only for the two-factor handoff |
| Database | PostgreSQL 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
- Quick start — the routes you just got, and the flow a user walks through.
- Configuration — the config file end to end.
- Keeping the token table small — schedule the purge command.