Email Templates
Pre-built React Email templates for common use cases
Template Overview
The email package includes 21 pre-built templates covering authentication, organization management, billing, and content notifications.
All templates are built with React Email components and support:
- Responsive design
- Light color scheme only (
email-layout.tsxsetscolor-scheme/supported-color-schemesto light) - i18n (English, Spanish)
- Consistent branding
Not all 21 are actually called from application code today. Auth, org-invitation, and subscription-lifecycle templates are triggered from Better Auth plugin hooks in packages/auth/server.ts. The rest (marked Unused below) are fully built but not currently invoked anywhere — wire them up where noted if you want that notification to actually go out.
Authentication & Security
Password Reset (wired — packages/auth/server.ts)
import { sendPasswordResetEmail } from '@repo/email';
await sendPasswordResetEmail({
email: 'user@example.com',
resetUrl: 'https://app.example.com/reset?token=abc123',
});| Prop | Type | Description |
|---|---|---|
email | string | Recipient email address |
resetUrl | string | Password reset link |
Email Verification (wired — packages/auth/server.ts)
import { sendEmailVerificationEmail } from '@repo/email';
await sendEmailVerificationEmail({
email: 'user@example.com',
userName: 'John',
verificationUrl: 'https://app.example.com/verify?token=abc123',
});| Prop | Type | Description |
|---|---|---|
email | string | Recipient email address |
userName | string? | User's display name |
verificationUrl | string | Verification link |
Magic Link (wired — packages/auth/server.ts)
import { sendMagicLinkEmail } from '@repo/email';
await sendMagicLinkEmail({
email: 'user@example.com',
magicLinkUrl: 'https://app.example.com/auth/magic-link?token=abc123',
expiresInMinutes: 10,
});| Prop | Type | Description |
|---|---|---|
email | string | Recipient email address |
magicLinkUrl | string | The one-time sign-in link |
expiresInMinutes | number? | Link expiration time (default: 10) |
Sign-In OTP (wired — packages/auth/server.ts)
import { sendSignInOTPEmail } from '@repo/email';
await sendSignInOTPEmail({
email: 'user@example.com',
otp: '123456',
expiresInMinutes: 5,
});| Prop | Type | Description |
|---|---|---|
email | string | Recipient email address |
otp | string | One-time passcode for the email-OTP sign-in flow |
expiresInMinutes | number? | OTP expiration time (default: 5) |
Verification OTP (wired — packages/auth/server.ts)
import { sendVerificationOTPEmail } from '@repo/email';
await sendVerificationOTPEmail({
email: 'user@example.com',
otp: '123456',
expiresInMinutes: 5,
});| Prop | Type | Description |
|---|---|---|
email | string | Recipient email address |
otp | string | One-time passcode used for email verification |
expiresInMinutes | number? | OTP expiration time (default: 5) |
Two-Factor OTP (wired — packages/auth/server.ts)
import { sendTwoFactorOTPEmail } from '@repo/email';
await sendTwoFactorOTPEmail({
email: 'user@example.com',
otp: '123456',
expiresInMinutes: 5,
});| Prop | Type | Description |
|---|---|---|
email | string | Recipient email address |
otp | string | One-time password code |
expiresInMinutes | number? | OTP expiration time (default: 5) |
Login from New Device (not currently used)
import { sendLoginNewDeviceEmail } from '@repo/email';
await sendLoginNewDeviceEmail({
email: 'user@example.com',
userName: 'John',
deviceInfo: 'Chrome on macOS',
location: 'San Francisco, CA',
ipAddress: '192.168.1.1',
loginTime: '2024-01-15 10:30 AM',
secureAccountUrl: 'https://app.example.com/security',
});Password Changed (not currently used)
import { sendPasswordChangedEmail } from '@repo/email';
await sendPasswordChangedEmail({
email: 'user@example.com',
userName: 'John',
changedAt: '2024-01-15 10:30 AM',
ipAddress: '192.168.1.1',
secureAccountUrl: 'https://app.example.com/security',
});Account Locked (not currently used)
import { sendAccountLockedEmail } from '@repo/email';
await sendAccountLockedEmail({
email: 'user@example.com',
userName: 'John',
lockReason: 'Too many failed login attempts',
failedAttempts: 5,
lockedAt: '2024-01-15 10:30 AM',
unlockTime: '2024-01-15 11:00 AM',
resetPasswordUrl: 'https://app.example.com/reset-password',
});Account Deleted (not currently used)
import { sendAccountDeletedEmail } from '@repo/email';
await sendAccountDeletedEmail({
email: 'user@example.com',
userName: 'John',
deletedAt: '2024-01-15 10:30 AM',
supportEmail: 'support@example.com',
});Organization Management
Organization Invitation (wired — packages/auth/server.ts)
import { sendOrgInvitationEmail } from '@repo/email';
await sendOrgInvitationEmail({
email: 'newuser@example.com',
inviterName: 'John Doe',
organizationName: 'Acme Corp',
role: 'Editor',
inviteUrl: 'https://app.example.com/invite?token=abc123',
expiresAt: '2024-01-22',
message: 'Looking forward to working with you!',
});Invitation Accepted (not currently used)
import { sendInvitationAcceptedEmail } from '@repo/email';
await sendInvitationAcceptedEmail({
email: 'owner@example.com',
ownerName: 'John',
newMemberName: 'Jane Smith',
newMemberEmail: 'jane@example.com',
organizationName: 'Acme Corp',
role: 'Editor',
joinedAt: '2024-01-15 10:30 AM',
teamUrl: 'https://app.example.com/team',
});Role Changed (not currently used)
import { sendRoleChangedEmail } from '@repo/email';
await sendRoleChangedEmail({
email: 'user@example.com',
userName: 'Jane',
organizationName: 'Acme Corp',
previousRole: 'Viewer',
newRole: 'Editor',
changedBy: 'John Doe',
changedAt: '2024-01-15 10:30 AM',
dashboardUrl: 'https://app.example.com/dashboard',
});Removed from Organization (not currently used)
import { sendRemovedFromOrgEmail } from '@repo/email';
await sendRemovedFromOrgEmail({
email: 'user@example.com',
userName: 'Jane',
organizationName: 'Acme Corp',
removedBy: 'John Doe',
removedAt: '2024-01-15 10:30 AM',
supportEmail: 'support@example.com',
});Subscription & Billing
Subscription Started (wired — packages/auth/stripe-hooks.ts)
import { sendSubscriptionStartedEmail } from '@repo/email';
await sendSubscriptionStartedEmail({
email: 'user@example.com',
userName: 'John',
planName: 'Pro',
billingInterval: 'monthly',
amount: '$29.00',
nextBillingDate: '2024-02-15',
dashboardUrl: 'https://app.example.com/dashboard',
});Payment Receipt (not currently used)
import { sendPaymentReceiptEmail } from '@repo/email';
await sendPaymentReceiptEmail({
email: 'user@example.com',
userName: 'John',
invoiceNumber: 'INV-2024-001',
amount: '$29.00',
planName: 'Pro',
billingPeriod: 'Jan 15 - Feb 14, 2024',
paymentMethod: 'Visa ending in 4242',
paymentDate: '2024-01-15',
receiptUrl: 'https://app.example.com/receipts/INV-2024-001',
});Subscription Upgraded (wired — packages/auth/stripe-hooks.ts)
import { sendSubscriptionUpgradedEmail } from '@repo/email';
await sendSubscriptionUpgradedEmail({
email: 'user@example.com',
userName: 'John',
previousPlan: 'Basic',
newPlan: 'Pro',
newAmount: '$29.00',
billingInterval: 'monthly',
effectiveDate: '2024-01-15',
dashboardUrl: 'https://app.example.com/dashboard',
});Subscription Downgraded (wired — packages/auth/stripe-hooks.ts)
import { sendSubscriptionDowngradedEmail } from '@repo/email';
await sendSubscriptionDowngradedEmail({
email: 'user@example.com',
userName: 'John',
previousPlan: 'Pro',
newPlan: 'Basic',
newAmount: '$9.00',
billingInterval: 'monthly',
effectiveDate: '2024-02-15',
lostFeatures: ['Priority support', 'Advanced analytics', 'API access'],
dashboardUrl: 'https://app.example.com/dashboard',
});Subscription Cancelled (wired — packages/auth/stripe-hooks.ts)
import { sendSubscriptionCancelledEmail } from '@repo/email';
await sendSubscriptionCancelledEmail({
email: 'user@example.com',
userName: 'John',
planName: 'Pro',
accessUntil: '2024-02-15',
cancellationReason: 'User requested',
reactivateUrl: 'https://app.example.com/reactivate',
feedbackUrl: 'https://app.example.com/feedback',
});Content Notifications
Welcome Email (not currently used)
import { sendWelcomeEmail } from '@repo/email';
await sendWelcomeEmail({
email: 'user@example.com',
name: 'John',
});Article Published (not currently used)
import { sendArticlePublishedEmail } from '@repo/email';
await sendArticlePublishedEmail({
email: 'author@example.com',
userName: 'John',
articleTitle: 'Getting Started with React',
articleExcerpt: 'Learn the basics of React in this comprehensive guide...',
articleUrl: 'https://blog.example.com/getting-started-react',
coverImageUrl: 'https://blog.example.com/images/react-cover.jpg',
publishedAt: '2024-01-15 10:30 AM',
dashboardUrl: 'https://app.example.com/dashboard',
analyticsUrl: 'https://app.example.com/analytics',
});Previewing Templates
The canonical way to preview templates is the dedicated apps/email app (see Getting Started):
pnpm --filter email devOpen http://localhost:3003 to view all templates in the browser. (packages/email also has its own pnpm dev script pointing at the same templates/ directory on port 3030 — either works, but apps/email is the one referenced elsewhere in these docs.)
Creating Custom Templates
- Create a new file in
packages/email/templates/:
// templates/my-custom-email.tsx
import { EmailLayout, Heading, EmailText, EmailButton } from '../components';
interface MyCustomEmailProps {
userName?: string;
actionUrl?: string;
}
export default function MyCustomEmail({
userName = 'there',
actionUrl = 'https://example.com',
}: MyCustomEmailProps) {
return (
<EmailLayout preview="Your custom email preview">
<Heading>Hello, {userName}!</Heading>
<EmailText>This is your custom email content.</EmailText>
<EmailButton href={actionUrl}>Take Action</EmailButton>
</EmailLayout>
);
}- Add the send function in
index.ts, using the package's internalsendEmail()helper (it wrapsgetEmailProvider()so individual send functions don't touch provider details directly):
import MyCustomEmail from './templates/my-custom-email';
export async function sendMyCustomEmail({
email,
userName,
actionUrl,
}: {
email: string;
userName?: string;
actionUrl?: string;
}) {
const result = await sendEmail({
to: email,
subject: 'Your Custom Subject',
react: MyCustomEmail({ userName, actionUrl }),
});
if (!result.success) {
throw result.error || new Error('Failed to send email');
}
return { success: true, messageId: result.messageId };
}Always use the shared components from ../components to maintain consistent styling across all templates.