BETTER-AUTH. UI
Plugins

Magic Link

Add passwordless email sign-in to your Solid/Zaidan authentication flow.

The Magic Link plugin adds a passwordless email sign-in flow to the copied Solid/Zaidan auth UI. Users enter an email address, receive a one-time link, and sign in when they open it.

It contributes:

  • A copied <MagicLink /> view rendered at /auth/magic-link
  • A copied <MagicLinkSent /> confirmation view rendered at /auth/magic-link-sent
  • A Magic Link toggle button that links between password sign-in and the magic-link view
  • Solid runtime wiring through signInMagicLinkOptions

Setup

Install the Better Auth plugin

Add magicLink({ sendMagicLink }) to your Better Auth server config and connect sendMagicLink to your email provider:

src/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` with your provider.
      } 
    }) 
  ]
})

Replace example email logging with a real email provider before production.

Install the matching Solid client plugin

Add magicLinkClient() to your Solid auth client so authClient.signIn.magicLink is available:

src/lib/auth-client.ts
import { magicLinkClient } from "better-auth/client/plugins"
import { createAuthClient } from "better-auth/solid"

export const authClient = createAuthClient({
  plugins: [
    magicLinkClient() 
  ]
})

Install the Solid/Zaidan components

npx shadcn@latest add https://better-auth-ui.com/r/solid/magic-link.json

This copies the following files into your project:

  • src/lib/auth/magic-link-plugin.ts
  • src/components/auth/magic-link.tsx
  • src/components/auth/magic-link-sent.tsx
  • src/components/auth/magic-link-button.tsx
  • src/components/auth/open-email-button.tsx
  • local form UI primitives under src/components/ui/**

Register the UI plugin

Register magicLinkPlugin() in your copied Solid auth provider:

src/components/providers.tsx
import { AuthProvider } from "@/components/auth/auth-provider"
import { magicLinkPlugin } from "@/lib/auth/magic-link-plugin"

<AuthProvider
  authClient={authClient}
  plugins={[magicLinkPlugin()]} 
>
  {children}
</AuthProvider>

Allow the plugin auth route

The copied route must accept the plugin-contributed auth path. Keep provider registration and route validation aligned, especially if you customize magicLinkPlugin({ path }):

src/routes/auth/$path.tsx
import { viewPaths } from "@better-auth-ui/core"
import { createFileRoute, redirect } from "@tanstack/solid-router"

import { Auth } from "@/components/auth/auth"
import { magicLinkPlugin } from "@/lib/auth/magic-link-plugin"

const validAuthPathSegments = new Set([
  ...Object.values(viewPaths.auth),
  ...Object.values(magicLinkPlugin().viewPaths.auth) 
])

export const Route = createFileRoute("/auth/$path")({
  beforeLoad({ params: { path } }) {
    if (!validAuthPathSegments.has(path)) {
      throw redirect({ to: "/" })
    }
  },
  component: AuthPage
})

function AuthPage() {
  const { path } = Route.useParams()()
  return <Auth path={path} />
}

/auth/magic-link and /auth/magic-link-sent work after both the provider registration step and the route-wiring step are in place.

Components

The copied <MagicLink /> view is rendered when the Magic Link plugin is registered and the auth route accepts the plugin path.

import { MagicLink } from "@/components/auth/magic-link"

<MagicLink />

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 redirects back to the magic-link form when that state is missing and 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 "@/components/auth/magic-link-sent"

<MagicLinkSent />

Options

Override the route segment or localization by configuring the copied magicLinkPlugin() factory:

src/lib/auth/magic-link-plugin.ts
magicLinkPlugin({
  path: "email-link",
  sentPath: "email-link-sent",
  localization: {
    sendMagicLink: "Email me a link"
  }
})

If you override path or sentPath, use the same plugin options in both your provider registration and your auth-route validation.

Localization

In a custom Solid component, read Magic Link labels from the registered plugin metadata. This keeps the form and toggle button consistent.

Email template

You can pair sendMagicLink with the existing <MagicLinkEmail /> example, or render your own email template on the server.

Passwordless-only

If your app disables email and password auth, you can keep the Magic Link flow as the primary sign-in surface:

<AuthProvider
  authClient={authClient}
  emailAndPassword={{ enabled: false }}
  plugins={[magicLinkPlugin()]}
>
  {children}
</AuthProvider>

The copied plugin already provides fallbackViews.auth.signIn. The signUp, forgotPassword, resetPassword, and resetLinkSent views redirect there when password auth is disabled. Route behavior still depends on the setup steps above: register magicLinkPlugin() and allow both plugin auth paths in /auth/$path.

Last updated on

On this page