Email Service

Better Auth Infrastructure provides a managed transactional email service with pre-built templates for common authentication flows. Send verification emails, password resets, invitations, and more without managing email infrastructure.

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:

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

Quick Start

Send a Single Email

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

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

Create a Reusable Sender

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: "user@example.com",
  variables: {
    resetLink: "https://yourapp.com/reset?token=xyz",
    userEmail: "user@example.com",
  },
});

Available Templates

verify-email

Sends an email verification link to new users.

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

reset-password

Sends a password reset link.

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

change-email

Confirms an email address change request.

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

sign-in-otp

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

await sendEmail({
  template: "sign-in-otp",
  to: "user@example.com",
  variables: {
    otpCode: "123456",
    userEmail: "user@example.com",
    appName: "Your App",               // Optional
    expirationMinutes: "10",           // Optional
  },
});

verify-email-otp

Sends an OTP code for email verification.

await sendEmail({
  template: "verify-email-otp",
  to: "user@example.com",
  variables: {
    otpCode: "123456",
    userEmail: "user@example.com",
    appName: "Your App",               // Optional
    expirationMinutes: "10",           // Optional
  },
});

reset-password-otp

Sends an OTP code for password reset.

await sendEmail({
  template: "reset-password-otp",
  to: "user@example.com",
  variables: {
    otpCode: "123456",
    userEmail: "user@example.com",
    appName: "Your App",               // Optional
    expirationMinutes: "10",           // Optional
  },
});

Sends a magic link for passwordless authentication.

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

two-factor

Sends a two-factor authentication code.

await sendEmail({
  template: "two-factor",
  to: "user@example.com",
  variables: {
    otpCode: "123456",
    userEmail: "user@example.com",
    userName: "John",                   // Optional
    appName: "Your App",               // Optional
    expirationMinutes: "5",            // Optional
  },
});

invitation

Sends an organization invitation.

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

application-invite

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

await sendEmail({
  template: "application-invite",
  to: "newuser@example.com",
  variables: {
    inviteLink: "https://yourapp.com/join?token=abc",
    inviterName: "John Smith",
    inviterEmail: "john@company.com",
    inviteeEmail: "newuser@example.com",
    appName: "Your App",               // Optional
    expirationDays: "7",               // Optional
  },
});

delete-account

Sends account deletion confirmation.

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

stale-account-user

Notifies a user that their dormant account was accessed.

await sendEmail({
  template: "stale-account-user",
  to: "user@example.com",
  variables: {
    userEmail: "user@example.com",
    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.

await sendEmail({
  template: "stale-account-admin",
  to: "admin@yourapp.com",
  variables: {
    userEmail: "user@example.com",
    userId: "user_123",
    adminEmail: "admin@yourapp.com",
    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

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:

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

Response Format

SendEmailResult

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

Example Usage

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

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:

auth.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",
          },
        });
      },
    }),
  ],
});

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.