Back to Better Auth

Email Service

docs/content/docs/infrastructure/services/email.mdx

1.6.109.7 KB
Original Source

Overview

The email service offers:

  • Pre-built, professionally designed email templates
  • Multiple provider support (AWS SES, SendGrid, Resend)
  • Type-safe template variables
  • No infrastructure to manage
  • Deliverability optimization

Installation

The email service is included in the @better-auth/infra package:

ts
import { sendEmail, createEmailSender } from "@better-auth/infra";

Quick Start

Send a Single Email

ts
import { sendEmail } from "@better-auth/infra";

await sendEmail({
  template: "verify-email",
  to: "[email protected]",
  variables: {
    verificationUrl: "https://yourapp.com/verify?token=abc123",
    userEmail: "[email protected]",
    userName: "John",
    appName: "Your App",
  },
});

Create a Reusable Sender

ts
import { createEmailSender } from "@better-auth/infra";

const emailSender = createEmailSender({
  apiKey: process.env.BETTER_AUTH_API_KEY,
  apiUrl: process.env.BETTER_AUTH_API_URL,
});

// Send multiple emails
await emailSender.send({
  template: "reset-password",
  to: "[email protected]",
  variables: {
    resetLink: "https://yourapp.com/reset?token=xyz",
    userEmail: "[email protected]",
  },
});

Available Templates

verify-email

Sends an email verification link to new users.

ts
await sendEmail({
  template: "verify-email",
  to: "[email protected]",
  variables: {
    verificationUrl: "https://yourapp.com/verify?token=abc",
    userEmail: "[email protected]",
    verificationCode: "123456",        // Optional: for code-based verification
    userName: "John",                   // Optional
    appName: "Your App",               // Optional
    expirationMinutes: "60",           // Optional
  },
});

reset-password

Sends a password reset link.

ts
await sendEmail({
  template: "reset-password",
  to: "[email protected]",
  variables: {
    resetLink: "https://yourapp.com/reset?token=xyz",
    userEmail: "[email protected]",
    userName: "John",                   // Optional
    appName: "Your App",               // Optional
    expirationMinutes: "60",           // Optional
  },
});

change-email

Confirms an email address change request.

ts
await sendEmail({
  template: "change-email",
  to: "[email protected]",
  variables: {
    confirmationLink: "https://yourapp.com/confirm-email?token=abc",
    newEmail: "[email protected]",
    currentEmail: "[email protected]",
    userName: "John",                   // Optional
    appName: "Your App",               // Optional
    expirationMinutes: "60",           // Optional
  },
});

sign-in-otp

Sends a one-time password for passwordless sign-in.

ts
await sendEmail({
  template: "sign-in-otp",
  to: "[email protected]",
  variables: {
    otpCode: "123456",
    userEmail: "[email protected]",
    appName: "Your App",               // Optional
    expirationMinutes: "10",           // Optional
  },
});

verify-email-otp

Sends an OTP code for email verification.

ts
await sendEmail({
  template: "verify-email-otp",
  to: "[email protected]",
  variables: {
    otpCode: "123456",
    userEmail: "[email protected]",
    appName: "Your App",               // Optional
    expirationMinutes: "10",           // Optional
  },
});

reset-password-otp

Sends an OTP code for password reset.

ts
await sendEmail({
  template: "reset-password-otp",
  to: "[email protected]",
  variables: {
    otpCode: "123456",
    userEmail: "[email protected]",
    appName: "Your App",               // Optional
    expirationMinutes: "10",           // Optional
  },
});

Sends a magic link for passwordless authentication.

ts
await sendEmail({
  template: "magic-link",
  to: "[email protected]",
  variables: {
    magicLink: "https://yourapp.com/auth/magic?token=abc",
    userEmail: "[email protected]",
    appName: "Your App",               // Optional
    expirationMinutes: "15",           // Optional
  },
});

two-factor

Sends a two-factor authentication code.

ts
await sendEmail({
  template: "two-factor",
  to: "[email protected]",
  variables: {
    otpCode: "123456",
    userEmail: "[email protected]",
    userName: "John",                   // Optional
    appName: "Your App",               // Optional
    expirationMinutes: "5",            // Optional
  },
});

invitation

Sends an organization invitation.

ts
await sendEmail({
  template: "invitation",
  to: "[email protected]",
  variables: {
    inviteLink: "https://yourapp.com/invite?token=abc",
    inviterName: "John Smith",
    inviterEmail: "[email protected]",
    organizationName: "Acme Corp",
    role: "Member",
    appName: "Your App",               // Optional
    expirationDays: "7",               // Optional
  },
});

application-invite

Sends an application-level invitation (inviting users to the platform).

ts
await sendEmail({
  template: "application-invite",
  to: "[email protected]",
  variables: {
    inviteLink: "https://yourapp.com/join?token=abc",
    inviterName: "John Smith",
    inviterEmail: "[email protected]",
    inviteeEmail: "[email protected]",
    appName: "Your App",               // Optional
    expirationDays: "7",               // Optional
  },
});

delete-account

Sends account deletion confirmation.

ts
await sendEmail({
  template: "delete-account",
  to: "[email protected]",
  variables: {
    deletionLink: "https://yourapp.com/confirm-delete?token=abc",
    userEmail: "[email protected]",
    userName: "John",                   // Optional
    appName: "Your App",               // Optional
    expirationMinutes: "60",           // Optional
  },
});

stale-account-user

Notifies a user that their dormant account was accessed.

ts
await sendEmail({
  template: "stale-account-user",
  to: "[email protected]",
  variables: {
    userEmail: "[email protected]",
    daysSinceLastActive: "90",
    loginTime: "February 20, 2026, 3:45 PM UTC",
    userName: "John",                   // Optional
    appName: "Your App",               // Optional
    loginLocation: "New York, US",     // Optional
    loginDevice: "Chrome on Windows",  // Optional
    loginIp: "192.168.1.1",            // Optional
  },
});

stale-account-admin

Notifies an admin about dormant account reactivation.

ts
await sendEmail({
  template: "stale-account-admin",
  to: "[email protected]",
  variables: {
    userEmail: "[email protected]",
    userId: "user_123",
    adminEmail: "[email protected]",
    daysSinceLastActive: "90",
    loginTime: "February 20, 2026, 3:45 PM UTC",
    userName: "John",                   // Optional
    appName: "Your App",               // Optional
    loginLocation: "New York, US",     // Optional
    loginDevice: "Chrome on Windows",  // Optional
    loginIp: "192.168.1.1",            // Optional
  },
});

Configuration

EmailConfig

ts
interface EmailConfig {
  apiKey?: string;   // Your Better Auth Infrastructure API key
  apiUrl?: string;   // Custom API URL (optional)
}

Environment Variables

The email service automatically reads from environment variables:

dotenv
BETTER_AUTH_API_KEY=your_api_key_here
BETTER_AUTH_API_URL=https://api.betterauth.com  # Optional

Response Format

SendEmailResult

ts
interface SendEmailResult {
  success: boolean;
  messageId?: string;  // Email provider message ID
  error?: string;      // Error message if failed
}

Example Usage

ts
const result = await sendEmail({
  template: "verify-email",
  to: "[email protected]",
  variables: {
    verificationUrl: "https://yourapp.com/verify?token=abc",
    userEmail: "[email protected]",
  },
});

if (result.success) {
  console.log("Email sent:", result.messageId);
} else {
  console.error("Failed to send email:", result.error);
}

Plan Requirements

FeatureStarterProBusinessEnterprise
Transactional Email-YesYesYes

Transactional email is available on Pro plans and above.

Integration with Better Auth

The email service integrates seamlessly with Better Auth's authentication flows. Here's a complete example:

ts
import { betterAuth } from "better-auth";
import { organization } from "better-auth/plugins";
import { sendEmail } from "@better-auth/infra";

export const auth = betterAuth({
  emailAndPassword: {
    enabled: true,
    async sendResetPassword({ user, url }) {
      await sendEmail({
        template: "reset-password",
        to: user.email,
        variables: {
          resetLink: url,
          userEmail: user.email,
          userName: user.name,
          appName: "Your App",
        },
      });
    },
  },
  emailVerification: {
    sendOnSignUp: true,
    async sendVerificationEmail({ user, url }) {
      await sendEmail({
        template: "verify-email",
        to: user.email,
        variables: {
          verificationUrl: url,
          userEmail: user.email,
          userName: user.name,
          appName: "Your App",
        },
      });
    },
  },
  plugins: [
    organization({
      async sendInvitationEmail(data) {
        const inviteLink = `https://yourapp.com/accept-invitation/${data.id}`;
        await sendEmail({
          template: "invitation",
          to: data.email,
          variables: {
            inviteLink,
            inviterName: data.inviter.user.name,
            inviterEmail: data.inviter.user.email,
            organizationName: data.organization.name,
            role: data.role,
            appName: "Your App",
          },
        });
      },
    }),
  ],
});
<Callout type="warn"> Avoid awaiting the email sending in production to prevent timing attacks. On serverless platforms, use `waitUntil` or similar to ensure the email is sent without blocking the response. </Callout>