Skip to main content

Overview

Magic Link provides a passwordless sign-in experience. Users enter their email, receive a link, and click it to authenticate — no password needed.

Benefits

  • No password required — one-click authentication
  • Auto sign-up — new users are created automatically
  • Secure — links expire in 5 minutes and are one-time use
  • Uses existing infrastructure — same Nodemailer + Gmail SMTP setup

How It Works

1

User enters email

On the sign-in page, the user provides their email address.
2

Server generates a magic link

Better Auth creates a secure token (32 characters), stores it in the verification collection, and triggers the email callback.
3

Email is sent

In production, a styled email with the magic link is sent. In development, the link is logged to the console.
4

User clicks the link

The link hits /api/auth/magic-link/verify?token=..., Better Auth validates the token, creates a session, and redirects to the dashboard.

Server Configuration

The magic link plugin is configured in lib/better-auth/auth.ts:
import { magicLink } from "better-auth/plugins";
import { sendMagicLinkEmail } from "@/lib/nodemailer";

magicLink({
  expiresIn: 60 * 5, // 5 minutes
  disableSignUp: false,
  async sendMagicLink({ email, url, token }, request) {
    if (process.env.NODE_ENV !== "development") {
      await sendMagicLinkEmail({ email, magicLinkUrl: url });
    }
  },
})

Client Usage

import { authClient } from "@/lib/auth-client";

await authClient.signIn.magicLink({
  email: "user@example.com",
  callbackURL: "/dashboard",
  newUserCallbackURL: "/welcome",    // optional
  errorCallbackURL: "/error",        // optional
});

Testing

  1. Run npm run dev
  2. Go to the sign-in page and enter an email
  3. Check the terminal for the magic link URL:
🔐 MAGIC LINK (Development Mode)
To: test@example.com
Magic Link URL: http://localhost:3000/api/auth/magic-link/verify?token=abc123...
  1. Copy the URL and paste it in your browser

Customization

magicLink({
  expiresIn: 60 * 10, // 10 minutes instead of 5
})
magicLink({
  disableSignUp: true, // only existing users can sign in
})
magicLink({
  storeToken: "hashed", // extra security
})

Security

  • 5-minute expiry — links become invalid quickly
  • One-time use — each link can only be used once
  • Cryptographic tokens — 32-character secure random strings
  • HTTPS only in production
  • Auto-verified — users are marked as emailVerified: true