Skip to main content
This guide shows how to implement the sendUserInvitation function with popular email services.

Overview

The sendUserInvitation function is called whenever a private invitation (with an email address) is created. You’ll learn how to:
  • Integrate with Resend, SendGrid, and Nodemailer
  • Create beautiful HTML email templates
  • Handle both new invitations and role upgrades
  • Include dynamic data in your emails

Email Service Implementations

Using Resend

Resend is a modern email API with excellent TypeScript support.
1

Install Resend

npm install resend
2

Setup Environment

.env
RESEND_API_KEY=re_xxxxxxxxxxxxx
NEXT_PUBLIC_APP_URL=https://yourdomain.com
3

Configure Better Auth

lib/auth.ts
import { betterAuth } from "better-auth";
import { invite } from "better-auth-invite-plugin";
import { Resend } from "resend";

const resend = new Resend(process.env.RESEND_API_KEY);

export const auth = betterAuth({
  database: {
    // Your database config
  },
  plugins: [
    invite({
      async sendUserInvitation({ email, name, role, url, token, newAccount }) {
        const appName = "MyApp";
        const subject = newAccount
          ? `You're invited to join ${appName}!`
          : `Your role has been upgraded to ${role}`;

        await resend.emails.send({
          from: `${appName} <[email protected]>`,
          to: email,
          subject,
          html: `
            <!DOCTYPE html>
            <html>
              <head>
                <meta charset="utf-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
              </head>
              <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
                <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; text-align: center; border-radius: 10px 10px 0 0;">
                  <h1 style="color: white; margin: 0;">${newAccount ? '🎉 You\'re Invited!' : '⭐ Role Upgrade'}</h1>
                </div>
                
                <div style="background: #f9f9f9; padding: 30px; border-radius: 0 0 10px 10px;">
                  <h2 style="color: #333; margin-top: 0;">Hello ${name || 'there'}!</h2>
                  
                  ${newAccount 
                    ? `<p>You've been invited to join <strong>${appName}</strong> with the role of <strong>${role}</strong>.</p>`
                    : `<p>Great news! Your role has been upgraded to <strong>${role}</strong>. You now have access to additional features.</p>`
                  }
                  
                  <div style="text-align: center; margin: 30px 0;">
                    <a href="${url}" 
                       style="display: inline-block; background: #667eea; color: white; padding: 15px 30px; text-decoration: none; border-radius: 5px; font-weight: bold;">
                      ${newAccount ? 'Accept Invitation' : 'Activate New Role'}
                    </a>
                  </div>
                  
                  <p style="color: #666; font-size: 14px;">
                    If you didn't expect this invitation, you can safely ignore this email.
                  </p>
                  
                  <p style="color: #999; font-size: 12px; margin-top: 30px;">
                    Invitation code: <code style="background: #e0e0e0; padding: 2px 6px; border-radius: 3px;">${token}</code>
                  </p>
                </div>
              </body>
            </html>
          `,
        });
      },
    }),
  ],
});

Advanced Email Templates

1

Create Reusable Template Function

Extract your email template into a reusable function:
lib/email-templates.ts
export function renderInvitationEmail({
  name,
  role,
  url,
  token,
  newAccount,
  inviterName,
}: {
  name?: string;
  role: string;
  url: string;
  token: string;
  newAccount: boolean;
  inviterName?: string;
}) {
  const greeting = name ? `Hello ${name}!` : "Hello there!";
  const title = newAccount ? "You're Invited!" : "Role Upgrade";
  const message = newAccount
    ? `${inviterName ? `${inviterName} has invited` : 'You\'ve been invited'} you to join our platform as a <strong>${role}</strong>.`
    : `Your role has been upgraded to <strong>${role}</strong>!`;

  return `
    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>${title}</title>
      </head>
      <body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;">
        <div style="max-width: 600px; margin: 0 auto; background-color: #ffffff;">
          <!-- Header -->
          <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 40px; text-align: center;">
            <h1 style="color: white; margin: 0; font-size: 32px; font-weight: 700;">
              ${title}
            </h1>
          </div>
          
          <!-- Content -->
          <div style="padding: 40px;">
            <h2 style="color: #1a1a1a; margin: 0 0 20px 0; font-size: 24px;">${greeting}</h2>
            <p style="color: #4a4a4a; line-height: 1.8; font-size: 16px; margin: 0 0 30px 0;">
              ${message}
            </p>
            
            <!-- CTA Button -->
            <div style="text-align: center; margin: 40px 0;">
              <a href="${url}" 
                 style="display: inline-block; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 16px 40px; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 16px; box-shadow: 0 4px 6px rgba(102, 126, 234, 0.25);">
                ${newAccount ? 'Accept Invitation' : 'Activate New Role'}
              </a>
            </div>
            
            <p style="color: #6b6b6b; font-size: 14px; line-height: 1.6; margin: 30px 0 0 0;">
              If you're having trouble clicking the button, copy and paste this URL into your browser:
            </p>
            <p style="color: #667eea; word-break: break-all; font-size: 14px; margin: 10px 0 0 0;">
              ${url}
            </p>
          </div>
          
          <!-- Footer -->
          <div style="background-color: #f7f7f7; padding: 30px; border-top: 1px solid #e0e0e0;">
            <p style="color: #8a8a8a; font-size: 13px; margin: 0 0 10px 0; text-align: center;">
              This invitation was sent to ${name || 'you'} and is only valid for this email address.
            </p>
            <p style="color: #b0b0b0; font-size: 12px; margin: 0; text-align: center;">
              Invitation code: <code style="background: #e8e8e8; padding: 4px 8px; border-radius: 4px; font-family: 'Courier New', monospace;">${token}</code>
            </p>
          </div>
        </div>
      </body>
    </html>
  `;
}
2

Use Template in Better Auth

lib/auth.ts
import { invite } from "better-auth-invite-plugin";
import { renderInvitationEmail } from "./email-templates";
import { resend } from "./resend";

export const auth = betterAuth({
  // ... other config
  plugins: [
    invite({
      async sendUserInvitation(data) {
        const html = renderInvitationEmail({
          name: data.name,
          role: data.role,
          url: data.url,
          token: data.token,
          newAccount: data.newAccount,
          inviterName: "John Doe", // You can fetch this from the session
        });

        await resend.emails.send({
          from: "MyApp <[email protected]>",
          to: data.email,
          subject: data.newAccount
            ? "You're invited to join MyApp!"
            : `Your role has been upgraded to ${data.role}`,
          html,
        });
      },
    }),
  ],
});

Error Handling

Important: If the sendUserInvitation function throws an error, the invite creation will fail and return an error to the client. Always implement proper error handling.
lib/auth.ts
import { invite } from "better-auth-invite-plugin";

export const auth = betterAuth({
  // ... other config
  plugins: [
    invite({
      async sendUserInvitation(data) {
        try {
          await sendEmail({
            to: data.email,
            subject: "Invitation",
            html: `<a href="${data.url}">Accept</a>`,
          });
        } catch (error) {
          // Log the error for debugging
          console.error("Failed to send invitation email:", error);
          
          // Re-throw to prevent invite creation
          throw new Error("Failed to send invitation email");
        }
      },
    }),
  ],
});

Next Steps

Build docs developers (and LLMs) love