spatie/laravel-login-link
Drop the x-login-link Blade component into your login view to instantly sign in as any seeded test user—no more juggling credentials or roles. Links render only on localhost and allowed hosts, so it stays safe for local development.
Install and verify the wiring
composer require spatie/laravel-login-link
php artisan vendor:publish --tag="login-link-config" # optional but recommended
php artisan route:list | grep login-link # confirms the POST endpoint exists
The package auto-registers a single route (POST /laravel-login-link-login) and a Blade component alias x-login-link. There is nothing else to wire up.
Where to look first
config/login-link.php — the seven keys that govern everything (allowed_environments, allowed_hosts, automatically_create_missing_users, user_model, redirect_route_name, login_link_controller, middleware).x-login-link component markup — publish it with php artisan vendor:publish --tag="login-link-views" so you can see exactly what HTML/form/CSRF token gets rendered before you style it.LoginLinkController — publish-and-override this as soon as you need any behaviour the config doesn't cover.First use case: drop the component into your login view, guarded by environment.
@env('local')
<x-login-link email="[email protected]" label="Login as admin" />
<x-login-link email="[email protected]" label="Login as regular user" />
@endenv
If you browse to http://localhost:8000/login and click, you're in. If you browse to http://127.0.0.1:8000/login, you will not be — see Gotchas.
Role/state matrix on the login screen. The highest-value pattern is one link per persona you actually debug as, each pinned by email (stable, readable) rather than by key (fragile across migrate:fresh). Use :user-attributes only when the persona is defined by data rather than a known email:
<x-login-link :user-attributes="['role' => 'admin']" label="Any admin" />
<x-login-link :user-attributes="['role' => 'admin', 'is_active' => true]" label="Active admin" />
Note this resolves to the first matching row, so pair it with deterministic seeders or you'll login as a random fixture.
Deep-link into the page you're actually building. Because the default redirect is the last intended URL, you can hit a protected route, get bounced to login, click the link, and land where you wanted. When you want to override, prefer the component attribute (a concrete URL) over redirect_route_name (a global route name):
<x-login-link email="[email protected]" redirect-url="{{ route('admin.dashboard') }}" />
Multiple guards / admin panels. For a separate admin guard pointing at a different provider:
<x-login-link guard="admin" email="[email protected]" />
If you pass user_model instead, be aware the guard defaults to null (i.e. the default guard), so Auth::guard('admin')->user() will be null even though Auth::user() is populated. Don't mix the two knobs and expect the admin guard's session to be filled.
Inertia (Vue/React). Share the environment from HandleInertiaRequests, then import the shipped component straight out of vendor/:
'environment' => app()->environment(),
import LoginLink from '@/../../vendor/spatie/laravel-login-link/resources/js/login-link.vue';
<LoginLink v-if="$page.props.environment === 'local'" label="Login as admin" />
This relies on your @ alias resolving to resources/js; if you use a custom alias or a non-standard Vite root, copy the component into your app instead of importing across the vendor boundary.
Non-Vue/React front ends. Skip the component and POST with a CSRF token. Accepted fields: email, key, redirect_url, user_attributes. This is the same surface the Blade component uses, so anything you can do in Blade you can do from Alpine, Livewire, or a curl script.
Custom controller for team workflows. Publish the config, point login_link_controller at your own subclass, and add the things a team eventually needs: log who logged in as whom, restrict by IP, stamp a logged_in_via_link session flag so you can render a "you are impersonating" banner, or add a ?as= allowlist so nobody can log in as a super-admin by hand-editing the email.
The host allowlist is exact and excludes ports and schemes. Default is ['localhost']. php artisan serve on 127.0.0.1:8000 will throw, because the request host is 127.0.0.1. Herd/Valet users on myapp.test will throw too. Add every hostname you actually type into the address bar:
'allowed_hosts' => ['localhost', '127.0.0.1', 'myapp.test'],
If you're behind a proxy or TrustProxies is configured, getHost() reflects the forwarded host — allowlist that, not the internal one.
Failures are exceptions, not 404s. A wrong environment or host throws, which is loud by design but means a broken login link looks like a 500. Check APP_ENV and the current hostname first, then php artisan config:clear — a cached config will happily serve stale allowed_hosts and allowed_environments.
php artisan config:cache is the #1 silent killer in this package. If a teammate cached config in local, edits to login-link.php do nothing until they clear it.
Auto-creation produces a factory user, not a seeded user. automatically_create_missing_users is true by default, so a typo'd email silently creates a fresh row. That user won't have your seeder's roles, permissions, profile, team memberships, or tenant. Common follow-on symptoms:
email_verified_at null — set it in the factory.password.confirm prompt or 2FA challenge that you now can't pass because you don't know the generated password. Give dev users a known password in the factory.is_active, soft deletes) so the lookup misses.For repeatable work, seed the personas explicitly and set automatically_create_missing_users => false so mistakes surface immediately.
The link bypasses your login pipeline. Anything hanging off Fortify/Breeze's attempt flow — rate limiting, 2FA, Attempting/Authenticated listeners — won't run. Illuminate\Auth\Events\Login does fire, but verify your listeners (audit log, last_login_at, "new device" mailer) behave under a link login before you rely on them.
Never let this survive contact with a public host. Two independent guards, and you should rely on both: wrap every render in @env('local'), and never commit a allowed_environments/allowed_hosts change that could reach staging or production. Treat any diff to this config file as a security review item. A PR that adds 'staging' should get a comment, not an approval.
Feature tests: the test environment is testing and the host is typically localhost, so posting to the endpoint in a test throws unless you add 'testing' to allowed_environments (or config()->set() it in setUp). Usually the right answer is to keep using $this->actingAs($user) in tests and reserve the login link for humans.
Styling: the class attribute replaces the default underline rather than appending, so write the full class list (class="underline text-red-500"). If your Tailwind content globs don't include vendor/, classes used only inside the published view can get purged in production builds — another reason to publish and own the view.
Extension points, in order of preference: config keys → published views → custom controller via login_link_controller → your own POST call from JS. Reach for the controller only when you need behaviour, and remember that changing middleware from ['web'] removes the session and CSRF handling the endpoint depends on.
How can I help you explore Laravel packages today?