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. A value that is not a number, or is below 1, keeps 900, and a fraction above 1 counts by its whole part.
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_uses1—Default redemptions per link (1 = single-use). Override per link via issueLink($user, maxUses: N).

One-time codes​

KeyDefaultEnvironment variablePurpose
code_length8—One-time code length.
code_alphabet'ABCDEFGHJKMNPQRSTUVWXYZ23456789'—Alphabet for codes (governs keyspace); omits visually ambiguous characters.
max_attempts_per_token5EMAIL_MAGIC_LINK_MAX_ATTEMPTS_PER_TOKENHard per-token lockout for code mode. A value that is not a number, or is below 1, keeps 5, and a fraction above 1 counts by its whole part.
entropy_safety_factor1_000_000—Guardrail 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.
eligibilitynull (everyone signs in)—SignInEligibility implementation, asked when a credential is issued and again when it is redeemed.
token_storenull (bundled)—TokenStore implementation.
captchanull—CaptchaGuard implementation, run before any user lookup.
notificationMagicLinkNotification::class—Notification class (extend it to customize).
mail_identitynull (app.name, bundled greeting)—MailIdentity implementation, asked with the account in the issuing request for the application's name and the greeting of the sign-in mail.

Routing​

KeyDefaultEnvironment variablePurpose
routes.prefix''—Route prefix.
routes.middleware['web']—Route middleware (sessions + CSRF).
routes.redirect_to'/'—Fallback redirect after a login this package completes; a two-factor user lands where Fortify redirects after its challenge.
routes.intendedtrue—Return to the originally requested URL after login.
api.enabledfalse—Direct JSON token exchange for SPA and mobile clients.

Staying signed in​

KeyDefaultEnvironment variablePurpose
remember.enabledfalseEMAIL_MAGIC_LINK_REMEMBEROffer "Stay signed in" on the confirmation screen and the code form. A ticked box sets Laravel's remember cookie, which lasts 400 days unless the guard sets remember in config/auth.php; while this is off, a posted remember field has no effect. Each guard's user table needs Laravel's remember_token column, or a ticked sign-in fails after the link or code is spent. Accepting an invitation never sets the cookie.
KeyDefaultEnvironment variablePurpose
invalid_response.via'redirect'—Browser response for an invalid or expired link, a refused code and a dead invitation: '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_tonull—Redirect target for the 'redirect' strategy; null sends the request back to the form it came from, the sign-in form or the code form.
invalid_response.abort_status403—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​

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_noncenull—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().
ui.header_viewnull—View rendered above the card on every screen this package draws, in both view trees — a wordmark, a language switcher. A name that resolves to no view is dropped and logged at warning, never thrown.
ui.footer_viewnull—The same slot below the card. Same fallback: an unresolvable name spoils nothing.
ui.layoutnull—A Blade component of yours that the WireKit screens render inside, named as the tag without x- ('layouts.auth'). It receives the page's title as title and the card as its slot; its <head>, frame and theme are yours, and header_view, footer_view, html_class, vite and styles are not read while it is set. A name that resolves to no component is dropped and logged at warning, and the screens keep their own layout.
ui.heading_sizenull—Size of the main heading on the WireKit screens, one of WireKit's heading sizes (sm to 5xl). The level stays 1. null leaves WireKit's size for a level-one heading; an unknown size is logged and drawn at WireKit's fallback. The plain Blade screens do not read it.
ui.html_classnull—Class attribute of the WireKit layout's <html> element. WireKit switches its tokens to the dark scheme under .dark, so 'dark' renders the screens dark. null renders no attribute. The plain Blade screens do not read it.
ui.sign_in_urlnull—Your application's own sign-in page, as a URL or a path. After a link or a code was turned away, the screens of both view trees link to it beside the way to a new one. null links nothing. See After a link or a code was turned away.

Fortify bridge​

KeyDefaultEnvironment variablePurpose
fortify.mode'auto'EMAIL_MAGIC_LINK_FORTIFY'auto' (on if Fortify present), true, or false. An empty value counts as not set and stays 'auto'.
fortify.respect_two_factortrue—Route 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]EMAIL_MAGIC_LINK_REQUEST_MAX, EMAIL_MAGIC_LINK_REQUEST_PER_MINUTESDefaults the bundled request limiter uses.
limits.consume['max' => 10, 'per_minutes' => 1]EMAIL_MAGIC_LINK_CONSUME_MAX, EMAIL_MAGIC_LINK_CONSUME_PER_MINUTESDefaults the bundled consume limiter uses.
limits.invitation_view['max' => 30, 'per_minutes' => 1]EMAIL_MAGIC_LINK_INVITATION_VIEW_MAX, EMAIL_MAGIC_LINK_INVITATION_VIEW_PER_MINUTESDefaults the bundled invitation-display limiter uses. Higher than consume: it guards a page load, not a credential being spent. For each of the three limits, a value that is not a number, or is below 1, keeps the default, and a fraction above 1 counts by its whole part.

Token pruning​

KeyDefaultEnvironment variablePurpose
prune.schedulefalse—Register the purge in your scheduler, at prune.frequency. Off by default — see the purge command.
prune.frequency'daily'—hourly, daily, weekly or monthly. An unrecognized value falls back to daily.
prune.chunk1000—Rows one DELETE removes; the purge loops until nothing is left, so this bounds how long a single statement holds its row locks. A value above 30,000 is held to 30,000: the delete binds every id of its chunk, and a database refuses a statement past its parameter ceiling (32,766 on SQLite, 65,535 on PostgreSQL and MySQL).

Resend guard​

KeyDefaultEnvironment variablePurpose
resend.enabledtrueEMAIL_MAGIC_LINK_RESENDThrottling on this package's request endpoint. Keys your own application guards are unaffected. An empty value counts as not set and keeps it on.
resend.cooldown.base30—First cooldown in seconds after a send.
resend.cooldown.factor2—Multiplier applied to each subsequent cooldown.
resend.cooldown.max900—Ceiling for the escalating cooldown, in seconds.
resend.window.minutes60EMAIL_MAGIC_LINK_RESEND_WINDOW_MINUTESLength of the rolling window. A value that is not a number, or is below 1, keeps 60, and a fraction above 1 counts by its whole part.
resend.window.max_sends5EMAIL_MAGIC_LINK_RESEND_MAX_SENDSMaximum sends within that window. A value that is not a number, or is below 1, keeps 5, and a fraction above 1 counts by its whole part.
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. A value that is not a number, or is below 1, keeps 5, and a fraction above 1 counts by its whole part.
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. A value that is not a number, or is below 1, keeps 60, and a fraction above 1 counts by its whole part.
resend.storenull—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​

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. A value that is not a number, or is below 1, keeps 604800, and a fraction above 1 counts by its whole part.
invitations.storenull—Class implementing InvitationStore; null uses the bundled Eloquent store.
invitations.handlernull—Class implementing InvitationHandler — what accepting an invitation means in your application. Required when enabled.
invitations.viewnull—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_days30EMAIL_MAGIC_LINK_INVITATION_RETAIN_ACCEPTED_DAYSHow 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. A value that is not a number, or is negative, keeps 30, and a fraction counts by its whole part.

Replacing the models​

KeyDefaultEnvironment variablePurpose
models[]—Your subclasses of MagicLinkToken and Invitation, keyed by the package class. The package then uses your class on every path: every query, every row it writes, and the connection a claim runs on. A class that does not exist, or does not extend the package class, is ignored in favor of the package class.
'models' => [
\EmailMagicLink\Models\MagicLinkToken::class => \App\Models\MagicLinkToken::class,
],

Use it when you need your own relations, scopes or casts on a token or an invitation row. Your subclass inherits the package's table name, so it reads and writes the same rows — keep it that way, since a subclass with its own $table points the package at a table its migrations never created. In your own code, MagicLinkToken::model() returns the configured class and MagicLinkToken::resolve() a new instance of it.

This is a lighter tool than the store contracts. Bind your own TokenStore or InvitationStore when you want to change how tokens are stored; map a subclass here when the storage is fine and you only need more on the row.