Configuration reference
All values live in config/email-magic-link.php. The narrative walkthrough — which decision
each key belongs to — is in Configuration.
Channel
| Key | Default | Environment variable | Purpose |
|---|---|---|---|
enabled | true | EMAIL_MAGIC_LINK_ENABLED | Master switch for the channel (routes, notifications, limiters). |
mode | 'link' | EMAIL_MAGIC_LINK_MODE | 'link', 'code', or 'both'. |
Token lifetime and redemption
| Key | Default | Environment variable | Purpose |
|---|---|---|---|
ttl | 900 | EMAIL_MAGIC_LINK_TTL | Default token lifetime in seconds. |
link_ttl | null | EMAIL_MAGIC_LINK_LINK_TTL | Link lifetime in seconds; inherits ttl when unset or non-positive. |
code_ttl | null | EMAIL_MAGIC_LINK_CODE_TTL | Code lifetime in seconds; inherits ttl when unset (handy for a shorter, hand-typed code). |
max_uses | 1 | — | Default redemptions per link (1 = single-use). Override per link via issueLink($user, maxUses: N). |
One-time codes
| Key | Default | Environment variable | Purpose |
|---|---|---|---|
code_length | 8 | — | One-time code length. |
code_alphabet | 'ABCDEFGHJKMNPQRSTUVWXYZ23456789' | — | Alphabet for codes (governs keyspace); omits visually ambiguous characters. |
max_attempts_per_token | 5 | — | Hard per-token lockout for code mode. |
entropy_safety_factor | 1_000_000 | — | Guardrail bar; cannot be lowered below this floor. |
Guards and resolution
| Key | Default | Environment variable | Purpose |
|---|---|---|---|
guard | application default | EMAIL_MAGIC_LINK_GUARD | Default stateful guard to log into. |
guards | [] | — | Extra guards a request may select via a guard field. |
user_lookup | null (bundled) | — | UserLookup implementation. |
token_store | null (bundled) | — | TokenStore implementation. |
captcha | null | — | CaptchaGuard implementation, run before any user lookup. |
notification | MagicLinkNotification::class | — | Notification class (extend it to customize). |
Routing
| Key | Default | Environment variable | Purpose |
|---|---|---|---|
routes.prefix | '' | — | Route prefix. |
routes.middleware | ['web'] | — | Route middleware (sessions + CSRF). |
routes.redirect_to | '/' | — | Fallback redirect after login. |
routes.intended | true | — | Return to the originally requested URL after login. |
api.enabled | false | — | Direct JSON token exchange for SPA and mobile clients. |
Invalid or expired links
| Key | Default | Environment variable | Purpose |
|---|---|---|---|
invalid_response.via | 'redirect' | — | Browser response for an invalid or expired link: 'redirect', 'view', 'abort', 'json', or a custom InvalidLinkResponder class-string. |
invalid_response.view | 'email-magic-link::invalid' | — | View the 'view' strategy renders (receives a message), answered with abort_status. |
invalid_response.redirect_to | null | — | Redirect target for the 'redirect' strategy; null keeps the sign-in form. |
invalid_response.abort_status | 403 | — | HTTP status the 'abort' and 'view' strategies answer with. |
invalid_response.error_code | 'invalid_or_expired' | — | Stable JSON error code (JSON clients and the 'json' strategy). |
User interface
| Key | Default | Environment variable | Purpose |
|---|---|---|---|
ui.mode | 'auto' | EMAIL_MAGIC_LINK_UI | 'auto' (WireKit views if installed) or 'blade'. |
ui.vite | ['resources/css/app.css'] | — | Vite entries the WireKit layout loads; false or [] for a non-Vite host. |
ui.styles | [] | — | Plain stylesheet URLs to <link> into the WireKit layout. |
ui.script_nonce | null | — | Class implementing ScriptNonce, supplying the CSP nonce for every tag the bundled screens emit (the countdown script, the inline stylesheet, WireKit's tags). null reads the csp-nonce container binding spatie/laravel-csp registers, then a global csp_nonce(). |
Fortify bridge
| Key | Default | Environment variable | Purpose |
|---|---|---|---|
fortify.mode | 'auto' | EMAIL_MAGIC_LINK_FORTIFY | 'auto' (on if Fortify present), true, or false. |
fortify.respect_two_factor | true | — | Route users with two-factor enabled through the challenge. |
fortify.challenge_route | 'two-factor.login' | — | Fortify challenge route name. |
Rate limiting
| Key | Default | Environment variable | Purpose |
|---|---|---|---|
limiters.request | 'email-magic-link:request' | — | Named limiter on the request endpoint; override with RateLimiter::for(). |
limiters.consume | 'email-magic-link:consume' | — | Named limiter on the three endpoints that spend a credential. |
limiters.invitation_view | 'email-magic-link:invitation-view' | — | Named limiter on the invitation display page, which spends nothing — its own budget, so viewing an invitation cannot use up the allowance accepting one needs. |
limits.request | ['max' => 5, 'per_minutes' => 1] | — | Defaults the bundled request limiter uses. |
limits.consume | ['max' => 10, 'per_minutes' => 1] | — | Defaults the bundled consume limiter uses. |
limits.invitation_view | ['max' => 30, 'per_minutes' => 1] | — | Defaults the bundled invitation-display limiter uses. Higher than consume: it guards a page load, not a credential being spent. |
Token pruning
| Key | Default | Environment variable | Purpose |
|---|---|---|---|
prune.schedule | false | — | Register the daily purge in your scheduler. Off by default — see the purge command. |
prune.frequency | 'daily' | — | hourly, daily, weekly or monthly. An unrecognized value falls back to daily. |
prune.chunk | 1000 | — | Rows one DELETE removes; the purge loops until nothing is left, so this bounds how long a single statement holds its row locks. |
Resend guard
| Key | Default | Environment variable | Purpose |
|---|---|---|---|
resend.enabled | true | EMAIL_MAGIC_LINK_RESEND | Throttling on this package's request endpoint. Keys your own application guards are unaffected. |
resend.cooldown.base | 30 | — | First cooldown in seconds after a send. |
resend.cooldown.factor | 2 | — | Multiplier applied to each subsequent cooldown. |
resend.cooldown.max | 900 | — | Ceiling for the escalating cooldown, in seconds. |
resend.window.minutes | 60 | — | Length of the rolling window. |
resend.window.max_sends | 5 | — | Maximum sends within that window. |
lock_store | null | EMAIL_MAGIC_LINK_LOCK_STORE | Cache store the issuance lock uses; null uses the default. Its locks have to really exclude — the null driver implements the lock contract and hands out a lock that always succeeds, so the package refuses it rather than pretending to serialize. Sibling of resend.store: pointing only that one at a lockable store used to fix the resend guard and leave issuance throwing. |
lock_block_seconds | 5 | EMAIL_MAGIC_LINK_LOCK_BLOCK_SECONDS | How long a second programmatic issuance for the same address waits for the first to finish. It does not apply to the sign-in request endpoint, which never waits: the lock is taken only for an address that resolves to a user, so queueing for it would be a readable answer to whether that account exists. A latency ceiling for the callers it does govern — a waiter holds a worker for this long. |
lock_hold_seconds | 60 | EMAIL_MAGIC_LINK_LOCK_HOLD_SECONDS | The lock's TTL — how long it survives a process that died holding it. Not the same budget as the wait: a TTL shorter than the issuance it protects expires mid-work and lets a second request in, which is the one failure the lock exists to prevent and the one that leaves no trace. |
resend.store | null | — | Cache store for the guard; null uses the default. Must support atomic locks. The guard's state is a cache entry: cache:clear resets every cooldown, and the array store holds it for one process only. |
Invitations
| Key | Default | Environment variable | Purpose |
|---|---|---|---|
invitations.enabled | false | EMAIL_MAGIC_LINK_INVITATIONS_ENABLED | Register the invitation channel. Requires handler and view; the package refuses to boot without them. |
invitations.ttl | 604800 | EMAIL_MAGIC_LINK_INVITATION_TTL | Invitation lifetime in seconds (7 days). Floored at 60. |
invitations.store | null | — | Class implementing InvitationStore; null uses the bundled Eloquent store. |
invitations.handler | null | — | Class implementing InvitationHandler — what accepting an invitation means in your application. Required when enabled. |
invitations.view | null | — | Your acceptance screen. The package ships none. Required when enabled. |
invitations.redirect_to | '/' | — | Where to send someone whose acceptance produced no session. |
invitations.retain_accepted_days | 30 | — | How long accepted and revoked rows survive the purge. They carry the invited address in the clear, so this is a retention decision; 0 deletes them as soon as they settle. |