Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Email Magic Link For Laravel Laravel Package

pushery/email-magic-link-for-laravel

Passwordless authentication for Laravel using emailed magic links. Let users sign in with a single click, no passwords needed. Includes signed, expiring links, customizable views and routes, and simple setup for secure, friction-free login.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package with Composer, then publish and run its migrations before deploying any code that passes a scope — the scope column must exist first. If you intend to brand the sign-in mail or wrap the sign-in screens in your app's layout, publish the config too.

  1. composer require the package and let Laravel's package discovery register the service provider.
  2. Publish the migrations (php artisan vendor:publish for the package's migration tag) and run php artisan migrate. Confirm the new nullable scope column exists on the invitations table.
  3. Publish the config file and review the two identity hooks: mail_identity (a class implementing MailIdentity) and ui.layout (a Blade component name). Both are optional; leaving them unset keeps the bundled name/greeting and the bundled screen layout.
  4. Check whether you bind the package's InvitationStore / InvitationIssuer contracts yourself or rely on the defaults. If you have your own implementations, update them first (see Gotchas).
  5. First use case: invite an address on behalf of a tenant — invite($email, scope: 'workspace-7') — accept through the package's signed acceptance route, and branch on $scope in the acceptance view if you render tenant-specific chrome. Then, if the package also issues sign-in credentials, point mail_identity at a class that names the tenant so link and code mails greet the recipient correctly.

Look first at the contracts (InvitationStore, InvitationIssuer, and now MailIdentity) and the two result objects (IssuedInvitation, AcceptedInvitation): that is the whole surface area you will touch day to day. Everything else — routes, notifications, the acceptance view, the sign-in screens — is plumbing you can override as needed. The mail that goes out is still MagicLinkNotification, and the sign-in screens are still the bundled WireKit screens; the new settings change what they render, not which classes are involved.

Implementation Patterns

Scope as tenant identity. Pass a stable, unique string derived from the tenant (ID, UUID, or slug) as scope on every invite() call from tenant context.

$issuer->invite($email, scope: "tenant:{$tenant->id}");
$issuer->revoke($email, scope: "tenant:{$tenant->id}");

Keep the derivation in one place (a helper or a small InvitationScope value object) so invite and revoke can never disagree about the format — a mismatch silently means "revoke nothing" or "revoke the wrong tenant's invitation".

Revoke narrowly, always. In a multi-tenant app, never call revoke() without a scope: an unscoped revoke withdraws every scope's invitation for that address. Treat the unscoped call as an admin/cleanup tool only.

Read the scope back. IssuedInvitation::$scope and AcceptedInvitation::$scope are your hooks for logging, audit trails, and post-acceptance redirects. In the acceptance view, $scope lets you render tenant branding and pick the post-accept destination (/t/{scope}/dashboard). Parse the scope back into a tenant model at the boundary rather than passing models around.

Compose with your own tenant resolution. A common pattern is a thin application-level service that wraps the issuer, resolves the current tenant, and stamps the scope — so controllers and jobs never hand-build scope strings:

class TenantInviter {
    public function __construct(private InvitationIssuer $issuer) {}
    public function invite(Tenant $tenant, string $email) {
        return $this->issuer->invite($email, scope: $tenant->invitationScope());
    }
}

Name the tenant in the sign-in mail. Implement MailIdentity and bind its class name to mail_identity. The issuing request calls it with the account, and the return value feeds the subject, introduction, sign-off and opening line of both the link mail and the code mail. Keep the implementation deterministic and tenant-aware:

class TenantMailIdentity implements MailIdentity
{
    public function forAccount(User $user): MailIdentityCopy
    {
        $tenant = $user->currentTenant(); // or resolve via the same scope helper

        return new MailIdentityCopy(
            appName: $tenant->name,
            greeting: "Welcome back to {$tenant->name}",
        );
    }
}

Return the same vocabulary you use elsewhere (app.name semantics, not a hard-coded product string), and never let it hit the database for another tenant — it runs on the request that issues the credential.

Adopt the host layout for sign-in screens. Point ui.layout at an existing Blade component that already renders your frame, <head>, title and theme. The package hands the page title in as title and the card as the slot, so the component needs to forward both:

{{-- resources/views/components/layouts/app.blade.php --}}
<!DOCTYPE html>
<html>
<head><title>{{ $title }}</title>…</head>
<body>
    <main>{{ $slot }}</main>
</body>
</html>

This is a layout, not a page — do not add sign-in markup yourself. It applies uniformly to the request, confirmation and code screens, so any per-screen styling stays in the package's card.

Callers are safe; implementers are not. All invitation calls remain source-compatible because $scope is last and optional. The upgrade cost lands entirely on classes that implement the contracts — typically test doubles, decorators, and auditing wrappers. MailIdentity is a brand-new contract, so there is nothing to retrofit; you only opt in.

Testing. Assert scope isolation explicitly: invite to the same address under two scopes, accept one, and verify the other remains open. Add a regression test for "re-invite under tenant A does not withdraw tenant B's invitation" — that is the bug this release fixes. For the new settings, assert that mail_identity changes the rendered subject/sign-off while the notification class stays MagicLinkNotification (existing assertions need no change), and assert that an unset or broken ui.layout still renders the bundled screens.

Gotchas and Tips

Breaking change: update your contract implementations before upgrading. Any class outside the package implementing InvitationStore (issue(), revoke()) or InvitationIssuer (invite(), revoke()) must add ?string $scope = null as the final parameter, or PHP throws a fatal signature-compatibility error on class load. This includes anonymous classes and mocks defined inline in tests. Grep for implements InvitationStore and implements InvitationIssuer before running the test suite.

Deploy ordering matters. Run the migration before shipping code that passes scope; otherwise inserts fail on an unknown column. The reverse order is safe (old code simply never sets it), so migrate first, deploy second.

Existing rows are unscoped (NULL). New behaviour is backward compatible only for applications that never name a scope: unscoped invites supersede unscoped ones, and scoped invites are invisible to that supersede logic. If you adopt scopes across an app that already has invitations in flight, backfill the column or expect a window where old unscoped invitations and new scoped ones coexist and do not supersede each other.

The supersede rule is scoped, the revoke rule is not. invite(..., scope: 'x') only withdraws open invitations with scope x; revoke() with no scope withdraws all of them. That asymmetry is intentional but easy to misread in code review — be explicit in call sites.

Named arguments are your friend. Since $scope is last and optional, prefer scope: '...' in call sites; it survives any future parameter additions and reads better than a positional literal.

Debugging scope problems. Query the invitations table directly and inspect the scope column first — most "invitation still exists / invitation vanished" bugs are a mismatched or missing scope string, or a revoked-without-scope call. Log the scope alongside the address on every issue/revoke.

Normalise scope strings. Trailing whitespace, casing, or using a tenant ID in one place and a slug in another produces invitations that look present but never match. Use one canonical builder for the string — and feed the same helper into MailIdentity so the mail names the tenant the invitation belongs to.

MailIdentity exceptions do not block delivery. The package catches and reports them, then sends under the bundled app.name and greeting. That is a safe fallback, not a guarantee of correct branding: if greeting correctness matters, test the fallback and monitor the reports. Keep the implementation cheap and non-throwing; it is on the credential-issuing request path. Without the setting the mail names app.name and opens with the bundled greeting, as before.

A bad ui.layout is a silent-ish fallback. A name that resolves to no component is dropped and logged at warning, and the screens keep their own layout — a typo looks like "the setting did nothing" rather than an error. Check the log, and confirm your component exists in the component namespace the package resolves against. Without the setting nothing changes.

title and the slot are both required. If your layout component ignores title you lose the page title; if it drops $slot, the card disappears. Keep it thin and reuse the same component your other pages already use, so theme and <head> stay consistent.

WireKit floor is documented, not enforced at runtime. composer suggest names 2.68.0 as the oldest supported release, but the sign-in screens still render on older WireKit. Treat this as a support boundary and an upgrade prompt, not a hard dependency — pin below it knowingly, and consult the release list beside the floor when a screen misbehaves. The floor is the point at which every part of the screens is known-good.

Extension points. If you need custom storage or delivery, implement the contracts (with the new signature) and rebind them in a service provider rather than forking the package; the acceptance view and notifications can be overridden through publishing/views in the usual Laravel way. Branding and chrome are deliberately configuration, not subclassing — use mail_identity and ui.layout before reaching for an override.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console