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 "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()]
})Install the UI plugin
Run the shadcn CLI to install the magic-link form, the toggle button, and the magicLinkPlugin() factory into your project:
npx shadcn@latest add https://better-auth-ui.com/r/magic-link.jsonThis drops the following into your codebase:
src/lib/auth/auth-plugin.ts— localAuthPlugintyping widenersrc/lib/auth/magic-link-plugin.ts—magicLinkPlugin()factorysrc/components/auth/magic-link.tsx— the magic-link formsrc/components/auth/magic-link-button.tsx— the toggle buttonsrc/components/auth/provider-button(s).tsx— social provider buttons used by the form
Register the plugin
Pass magicLinkPlugin() to <AuthProvider>:
import { magicLinkPlugin } from "@/lib/auth/magic-link-plugin"
import { AuthProvider } from "@/components/auth/auth-provider"
<AuthProvider
authClient={authClient}
navigate={navigate}
plugins={[magicLinkPlugin()]}
>
{children}
</AuthProvider>Allow the new view path
The plugin contributes a magic-link segment to viewPaths.auth. Spread magicLinkPlugin().viewPaths?.auth into your auth route's allowed-paths set so /auth/magic-link resolves correctly:
import { viewPaths } from "@better-auth-ui/core"
import { createFileRoute, notFound } from "@tanstack/react-router"
import { Auth } from "@/components/auth/auth"
import { magicLinkPlugin } from "@/lib/auth/magic-link-plugin"
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 { notFound } from "next/navigation"
import { Auth } from "@/components/auth/auth"
import { magicLinkPlugin } from "@/lib/auth/magic-link-plugin"
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 />
The <MagicLink /> view is rendered at /auth/magic-link when the plugin is registered and the route is wired up (see the setup step above).
Usage
import { MagicLink } from "@/components/auth/magic-link"
<MagicLink />Props
Prop
Type
Options
magicLinkPlugin({
// Override the URL segment. Default: "magic-link"
path: "email-link",
// 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 routes redirect to it.
Last updated on