BETTER-AUTH. UI
Plugins

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 useSignInMagicLink mutation 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:

lib/auth.ts
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:

lib/auth-client.ts
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.json

This drops the following into your codebase:

  • src/lib/auth/auth-plugin.ts — local AuthPlugin typing widener
  • src/lib/auth/magic-link-plugin.tsmagicLinkPlugin() factory
  • src/components/auth/magic-link.tsx — the magic-link form
  • src/components/auth/magic-link-button.tsx — the toggle button
  • src/components/auth/provider-button(s).tsx — social provider buttons used by the form

Register the plugin

Pass magicLinkPlugin() to <AuthProvider>:

components/providers.tsx
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:

routes/auth/$path.tsx
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} />
}
app/auth/[path]/page.tsx
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

Sign In
Continue with Password
OR

Need to create an account? Sign Up

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"
  }
})

Localization

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

On this page