Skip to main content

Configuration reference

All values live in config/email-magic-link.php. The narrative walkthrough — which decision each key belongs to — is in Configuration.

Channel

KeyDefaultEnvironment variablePurpose
enabledtrueEMAIL_MAGIC_LINK_ENABLEDMaster switch for the channel (routes, notifications, limiters).
mode'link'EMAIL_MAGIC_LINK_MODE'link', 'code', or 'both'.

Token lifetime and redemption

KeyDefaultEnvironment variablePurpose
ttl900EMAIL_MAGIC_LINK_TTLDefault token lifetime in seconds.
link_ttlnullEMAIL_MAGIC_LINK_LINK_TTLLink lifetime in seconds; inherits ttl when unset or non-positive.
code_ttlnullEMAIL_MAGIC_LINK_CODE_TTLCode lifetime in seconds; inherits ttl when unset (handy for a shorter, hand-typed code).
max_uses1Default redemptions per link (1 = single-use). Override per link via issueLink($user, maxUses: N).

One-time codes

KeyDefaultEnvironment variablePurpose
code_length8One-time code length.
code_alphabet'ABCDEFGHJKMNPQRSTUVWXYZ23456789'Alphabet for codes (governs keyspace); omits visually ambiguous characters.
max_attempts_per_token5Hard per-token lockout for code mode.
entropy_safety_factor1_000_000Guardrail bar; cannot be lowered below this floor.

Guards and resolution

KeyDefaultEnvironment variablePurpose
guardapplication defaultEMAIL_MAGIC_LINK_GUARDDefault stateful guard to log into.
guards[]Extra guards a request may select via a guard field.
user_lookupnull (bundled)UserLookup implementation.
token_storenull (bundled)TokenStore implementation.
captchanullCaptchaGuard implementation, run before any user lookup.
notificationMagicLinkNotification::classNotification class (extend it to customize).

Routing

KeyDefaultEnvironment variablePurpose
routes.prefix''Route prefix.
routes.middleware['web']Route middleware (sessions + CSRF).
routes.redirect_to'/'Fallback redirect after login.
routes.intendedtrueReturn to the originally requested URL after login.
api.enabledfalseDirect JSON token exchange for SPA and mobile clients.
KeyDefaultEnvironment variablePurpose
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_tonullRedirect target for the 'redirect' strategy; null keeps the sign-in form.
invalid_response.abort_status403HTTP 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

KeyDefaultEnvironment variablePurpose
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_noncenullClass 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

KeyDefaultEnvironment variablePurpose
fortify.mode'auto'EMAIL_MAGIC_LINK_FORTIFY'auto' (on if Fortify present), true, or false.
fortify.respect_two_factortrueRoute users with two-factor enabled through the challenge.
fortify.challenge_route'two-factor.login'Fortify challenge route name.

Rate limiting

KeyDefaultEnvironment variablePurpose
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

KeyDefaultEnvironment variablePurpose
prune.schedulefalseRegister 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.chunk1000Rows one DELETE removes; the purge loops until nothing is left, so this bounds how long a single statement holds its row locks.

Resend guard

KeyDefaultEnvironment variablePurpose
resend.enabledtrueEMAIL_MAGIC_LINK_RESENDThrottling on this package's request endpoint. Keys your own application guards are unaffected.
resend.cooldown.base30First cooldown in seconds after a send.
resend.cooldown.factor2Multiplier applied to each subsequent cooldown.
resend.cooldown.max900Ceiling for the escalating cooldown, in seconds.
resend.window.minutes60Length of the rolling window.
resend.window.max_sends5Maximum sends within that window.
lock_storenullEMAIL_MAGIC_LINK_LOCK_STORECache 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_seconds5EMAIL_MAGIC_LINK_LOCK_BLOCK_SECONDSHow 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_seconds60EMAIL_MAGIC_LINK_LOCK_HOLD_SECONDSThe 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.storenullCache 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

KeyDefaultEnvironment variablePurpose
invitations.enabledfalseEMAIL_MAGIC_LINK_INVITATIONS_ENABLEDRegister the invitation channel. Requires handler and view; the package refuses to boot without them.
invitations.ttl604800EMAIL_MAGIC_LINK_INVITATION_TTLInvitation lifetime in seconds (7 days). Floored at 60.
invitations.storenullClass implementing InvitationStore; null uses the bundled Eloquent store.
invitations.handlernullClass implementing InvitationHandler — what accepting an invitation means in your application. Required when enabled.
invitations.viewnullYour acceptance screen. The package ships none. Required when enabled.
invitations.redirect_to'/'Where to send someone whose acceptance produced no session.
invitations.retain_accepted_days30How 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.