Magic Link
Add passwordless email sign-in to your authentication flow.
The magic-link plugin adds a passwordless email sign-in flow. The user enters their email, receives a one-time link, and is signed in when they click it.
It contributes:
- A
<MagicLink />view at/auth/magic-link - A
<MagicLinkSent />confirmation view at/auth/magic-link-sent - A "Continue with Magic Link" button rendered alongside the password sign-in button
- A
useSignInMagicLinkmutation hook
When emailAndPassword.enabled === false, <MagicLink /> automatically takes over /auth/sign-in as the primary passwordless surface.
Setup
Install the Better Auth plugin
Add the Magic Link plugin to your Better Auth server config and wire up sendMagicLink to your email provider:
import { betterAuth } from "better-auth"
import { magicLink } from "better-auth/plugins"
export const auth = betterAuth({
// ...
plugins: [
magicLink({
sendMagicLink: async ({ email, url }) => {
// Send `url` to `email` via your email provider.
}
})
]
})Install the matching client plugin
Add magicLinkClient() to your auth client so authClient.signIn.magicLink is available:
import { createAuthClient } from "better-auth/react"
import { magicLinkClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
plugins: [magicLinkClient()]
})Register the UI plugin
Pass magicLinkPlugin() to <AuthProvider>:
import { AuthProvider } from "@better-auth-ui/heroui"
import { magicLinkPlugin } from "@better-auth-ui/heroui/plugins"
<AuthProvider
authClient={authClient}
navigate={navigate}
plugins={[magicLinkPlugin()]}
>
{children}
</AuthProvider>Allow the new view path
The plugin contributes magic-link and magic-link-sent segments to viewPaths.auth. Spread magicLinkPlugin().viewPaths?.auth into your auth route's allowed-paths set so both views resolve correctly:
import { viewPaths } from "@better-auth-ui/core"
import { Auth } from "@better-auth-ui/heroui"
import { magicLinkPlugin } from "@better-auth-ui/heroui/plugins"
import { createFileRoute, notFound } from "@tanstack/react-router"
export const Route = createFileRoute("/auth/$path")({
beforeLoad({ params: { path } }) {
if (
!Object.values({
...viewPaths.auth,
...magicLinkPlugin().viewPaths?.auth
}).includes(path)
) {
throw notFound()
}
},
component: AuthPage
})
function AuthPage() {
const { path } = Route.useParams()
return <Auth path={path} />
}import { viewPaths } from "@better-auth-ui/core"
import { Auth } from "@better-auth-ui/heroui"
import { magicLinkPlugin } from "@better-auth-ui/heroui/plugins"
import { notFound } from "next/navigation"
export default async function AuthPage({
params
}: {
params: Promise<{ path: string }>
}) {
const { path } = await params
if (
!Object.values({
...viewPaths.auth,
...magicLinkPlugin().viewPaths?.auth
}).includes(path)
) {
notFound()
}
return <Auth path={path} />
}Components
<MagicLink />
Sign In
OR
The <MagicLink /> view is automatically rendered at /auth/magic-link when the plugin is registered.
Usage
import { MagicLink } from "@better-auth-ui/heroui/plugins"
<MagicLink />Props
Prop
Type
<MagicLinkSent />
After a magic-link request succeeds, the form stores the submitted email in session storage and navigates to this confirmation view. It shows an email-provider shortcut when one is available. Hover or focus the button to show a QR code for opening the same provider URL on another device.
import { MagicLinkSent } from "@better-auth-ui/heroui/plugins"
<MagicLinkSent />Prop
Type
Options
magicLinkPlugin({
// Override the URL segment. Default: "magic-link"
path: "email-link",
// Override the confirmation segment. Default: "magic-link-sent"
sentPath: "email-link-sent",
// Override any of the plugin's localization strings.
localization: {
sendMagicLink: "Email me a link"
}
})Prop
Type
Localization
Prop
Type
Read these from useAuthPlugin(magicLinkPlugin).localization inside custom slot components.
Email template
Pair the plugin with the <MagicLinkEmail /> component to send a styled email from your sendMagicLink callback.
Passwordless-only flows
If you disable email + password auth entirely, the magic-link form is promoted to the primary sign-in view automatically — no extra config needed:
<AuthProvider
authClient={authClient}
navigate={navigate}
emailAndPassword={{ enabled: false }}
plugins={[magicLinkPlugin()]}
>
{children}
</AuthProvider>/auth/sign-in now renders <MagicLink />, and the signUp, forgotPassword, resetPassword, and resetLinkSent routes redirect to it.
Last updated on