Tutorial: Email System

Set up a complete email system with transactional emails, templates, and queue management.

What You'll Build#

  • Email provider integration (Resend)
  • React Email templates
  • Transactional email sending
  • Email queue with retries
  • Welcome, notification, and receipt emails

Prerequisites#

  • Next.js project
  • Resend account (free tier works)
  • Bootspring initialized

Time Required#

Approximately 25 minutes.

Step 1: Set Up Resend#

Create Resend Account#

  1. Go to resend.com and sign up
  2. Verify your domain (or use sandbox for testing)
  3. Create an API key

Add Environment Variables#

# .env.local RESEND_API_KEY=re_... EMAIL_FROM=noreply@yourdomain.com

Step 2: Apply Email Skill#

bootspring skill apply email/resend bootspring skill apply email/templates

Step 3: Install Dependencies#

npm install resend @react-email/components react-email

Step 4: Create Email Client#

1// lib/email.ts 2import { Resend } from 'resend'; 3 4if (!process.env.RESEND_API_KEY) { 5 throw new Error('RESEND_API_KEY is required'); 6} 7 8export const resend = new Resend(process.env.RESEND_API_KEY); 9 10export const EMAIL_FROM = process.env.EMAIL_FROM || 'noreply@example.com'; 11 12export type SendEmailOptions = { 13 to: string | string[]; 14 subject: string; 15 react: React.ReactElement; 16 text?: string; 17 replyTo?: string; 18}; 19 20export async function sendEmail({ 21 to, 22 subject, 23 react, 24 text, 25 replyTo, 26}: SendEmailOptions) { 27 try { 28 const result = await resend.emails.send({ 29 from: EMAIL_FROM, 30 to, 31 subject, 32 react, 33 text, 34 reply_to: replyTo, 35 }); 36 37 return { success: true, id: result.data?.id }; 38 } catch (error) { 39 console.error('Failed to send email:', error); 40 return { success: false, error }; 41 } 42}

Step 5: Create Email Templates#

Base Layout#

1// emails/components/Layout.tsx 2import { 3 Body, 4 Container, 5 Head, 6 Html, 7 Img, 8 Link, 9 Preview, 10 Section, 11 Tailwind, 12 Text, 13} from '@react-email/components'; 14 15interface LayoutProps { 16 preview: string; 17 children: React.ReactNode; 18} 19 20export function Layout({ preview, children }: LayoutProps) { 21 return ( 22 <Html> 23 <Head /> 24 <Preview>{preview}</Preview> 25 <Tailwind> 26 <Body className="bg-gray-100 font-sans"> 27 <Container className="mx-auto py-8 px-4"> 28 <Section className="bg-white rounded-lg shadow-sm p-8"> 29 <Img 30 src="https://yourdomain.com/logo.png" 31 alt="Logo" 32 width={120} 33 height={40} 34 className="mx-auto mb-8" 35 /> 36 {children} 37 </Section> 38 <Section className="mt-8 text-center"> 39 <Text className="text-gray-500 text-sm"> 40 © {new Date().getFullYear()} MyApp. All rights reserved. 41 </Text> 42 <Text className="text-gray-500 text-sm"> 43 <Link href="https://yourdomain.com/unsubscribe" className="underline"> 44 Unsubscribe 45 </Link> 46 {' | '} 47 <Link href="https://yourdomain.com/privacy" className="underline"> 48 Privacy Policy 49 </Link> 50 </Text> 51 </Section> 52 </Container> 53 </Body> 54 </Tailwind> 55 </Html> 56 ); 57}

Welcome Email#

1// emails/WelcomeEmail.tsx 2import { 3 Button, 4 Heading, 5 Hr, 6 Section, 7 Text, 8} from '@react-email/components'; 9import { Layout } from './components/Layout'; 10 11interface WelcomeEmailProps { 12 name: string; 13 loginUrl: string; 14} 15 16export function WelcomeEmail({ name, loginUrl }: WelcomeEmailProps) { 17 return ( 18 <Layout preview={`Welcome to MyApp, ${name}!`}> 19 <Heading className="text-2xl font-bold text-gray-900 text-center mb-4"> 20 Welcome to MyApp! 21 </Heading> 22 23 <Text className="text-gray-700 mb-4"> 24 Hi {name}, 25 </Text> 26 27 <Text className="text-gray-700 mb-4"> 28 Thank you for signing up! We're excited to have you on board. 29 Here's what you can do next: 30 </Text> 31 32 <Section className="bg-gray-50 rounded-lg p-4 mb-6"> 33 <Text className="text-gray-700 mb-2"> 34 <strong>1. Complete your profile</strong> - Add your details to personalize your experience. 35 </Text> 36 <Text className="text-gray-700 mb-2"> 37 <strong>2. Create your first project</strong> - Get started with your AI-assisted development. 38 </Text> 39 <Text className="text-gray-700 mb-0"> 40 <strong>3. Explore agents</strong> - Discover specialized AI experts. 41 </Text> 42 </Section> 43 44 <Section className="text-center mb-6"> 45 <Button 46 href={loginUrl} 47 className="bg-blue-600 text-white px-6 py-3 rounded-lg font-semibold" 48 > 49 Get Started 50 </Button> 51 </Section> 52 53 <Hr className="border-gray-200 my-6" /> 54 55 <Text className="text-gray-500 text-sm"> 56 If you have any questions, reply to this email or visit our{' '} 57 <a href="https://yourdomain.com/docs" className="text-blue-600"> 58 documentation 59 </a>. 60 </Text> 61 </Layout> 62 ); 63} 64 65export default WelcomeEmail;

Password Reset Email#

1// emails/PasswordResetEmail.tsx 2import { 3 Button, 4 Heading, 5 Section, 6 Text, 7} from '@react-email/components'; 8import { Layout } from './components/Layout'; 9 10interface PasswordResetEmailProps { 11 name: string; 12 resetUrl: string; 13 expiresIn: string; 14} 15 16export function PasswordResetEmail({ 17 name, 18 resetUrl, 19 expiresIn, 20}: PasswordResetEmailProps) { 21 return ( 22 <Layout preview="Reset your password"> 23 <Heading className="text-2xl font-bold text-gray-900 text-center mb-4"> 24 Reset Your Password 25 </Heading> 26 27 <Text className="text-gray-700 mb-4"> 28 Hi {name}, 29 </Text> 30 31 <Text className="text-gray-700 mb-4"> 32 We received a request to reset your password. Click the button below 33 to create a new password: 34 </Text> 35 36 <Section className="text-center mb-6"> 37 <Button 38 href={resetUrl} 39 className="bg-blue-600 text-white px-6 py-3 rounded-lg font-semibold" 40 > 41 Reset Password 42 </Button> 43 </Section> 44 45 <Text className="text-gray-500 text-sm mb-4"> 46 This link will expire in {expiresIn}. 47 </Text> 48 49 <Text className="text-gray-500 text-sm"> 50 If you didn't request this, you can safely ignore this email. 51 Your password will remain unchanged. 52 </Text> 53 </Layout> 54 ); 55} 56 57export default PasswordResetEmail;

Notification Email#

1// emails/NotificationEmail.tsx 2import { 3 Button, 4 Heading, 5 Section, 6 Text, 7} from '@react-email/components'; 8import { Layout } from './components/Layout'; 9 10interface NotificationEmailProps { 11 name: string; 12 title: string; 13 message: string; 14 actionUrl?: string; 15 actionText?: string; 16} 17 18export function NotificationEmail({ 19 name, 20 title, 21 message, 22 actionUrl, 23 actionText, 24}: NotificationEmailProps) { 25 return ( 26 <Layout preview={title}> 27 <Heading className="text-2xl font-bold text-gray-900 text-center mb-4"> 28 {title} 29 </Heading> 30 31 <Text className="text-gray-700 mb-4"> 32 Hi {name}, 33 </Text> 34 35 <Text className="text-gray-700 mb-6"> 36 {message} 37 </Text> 38 39 {actionUrl && actionText && ( 40 <Section className="text-center mb-6"> 41 <Button 42 href={actionUrl} 43 className="bg-blue-600 text-white px-6 py-3 rounded-lg font-semibold" 44 > 45 {actionText} 46 </Button> 47 </Section> 48 )} 49 </Layout> 50 ); 51} 52 53export default NotificationEmail;

Receipt Email#

1// emails/ReceiptEmail.tsx 2import { 3 Heading, 4 Hr, 5 Row, 6 Column, 7 Section, 8 Text, 9} from '@react-email/components'; 10import { Layout } from './components/Layout'; 11 12interface ReceiptEmailProps { 13 name: string; 14 receiptNumber: string; 15 date: string; 16 items: Array<{ 17 description: string; 18 amount: string; 19 }>; 20 total: string; 21} 22 23export function ReceiptEmail({ 24 name, 25 receiptNumber, 26 date, 27 items, 28 total, 29}: ReceiptEmailProps) { 30 return ( 31 <Layout preview={`Receipt #${receiptNumber}`}> 32 <Heading className="text-2xl font-bold text-gray-900 text-center mb-4"> 33 Payment Receipt 34 </Heading> 35 36 <Text className="text-gray-700 mb-4"> 37 Hi {name}, 38 </Text> 39 40 <Text className="text-gray-700 mb-6"> 41 Thank you for your payment. Here's your receipt: 42 </Text> 43 44 <Section className="bg-gray-50 rounded-lg p-4 mb-6"> 45 <Row> 46 <Column> 47 <Text className="text-gray-500 text-sm m-0">Receipt Number</Text> 48 <Text className="text-gray-900 font-medium m-0">{receiptNumber}</Text> 49 </Column> 50 <Column align="right"> 51 <Text className="text-gray-500 text-sm m-0">Date</Text> 52 <Text className="text-gray-900 font-medium m-0">{date}</Text> 53 </Column> 54 </Row> 55 </Section> 56 57 <Section className="mb-6"> 58 {items.map((item, index) => ( 59 <Row key={index} className="mb-2"> 60 <Column> 61 <Text className="text-gray-700 m-0">{item.description}</Text> 62 </Column> 63 <Column align="right"> 64 <Text className="text-gray-900 font-medium m-0">{item.amount}</Text> 65 </Column> 66 </Row> 67 ))} 68 69 <Hr className="border-gray-200 my-4" /> 70 71 <Row> 72 <Column> 73 <Text className="text-gray-900 font-bold m-0">Total</Text> 74 </Column> 75 <Column align="right"> 76 <Text className="text-gray-900 font-bold m-0">{total}</Text> 77 </Column> 78 </Row> 79 </Section> 80 81 <Text className="text-gray-500 text-sm"> 82 If you have any questions about this receipt, please contact our support team. 83 </Text> 84 </Layout> 85 ); 86} 87 88export default ReceiptEmail;

Step 6: Create Email Sending Functions#

1// lib/emails/send.ts 2import { sendEmail } from '@/lib/email'; 3import { WelcomeEmail } from '@/emails/WelcomeEmail'; 4import { PasswordResetEmail } from '@/emails/PasswordResetEmail'; 5import { NotificationEmail } from '@/emails/NotificationEmail'; 6import { ReceiptEmail } from '@/emails/ReceiptEmail'; 7 8export async function sendWelcomeEmail(to: string, name: string) { 9 return sendEmail({ 10 to, 11 subject: 'Welcome to MyApp!', 12 react: WelcomeEmail({ 13 name, 14 loginUrl: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard`, 15 }), 16 }); 17} 18 19export async function sendPasswordResetEmail( 20 to: string, 21 name: string, 22 token: string 23) { 24 return sendEmail({ 25 to, 26 subject: 'Reset your password', 27 react: PasswordResetEmail({ 28 name, 29 resetUrl: `${process.env.NEXT_PUBLIC_APP_URL}/reset-password?token=${token}`, 30 expiresIn: '1 hour', 31 }), 32 }); 33} 34 35export async function sendNotificationEmail( 36 to: string, 37 name: string, 38 title: string, 39 message: string, 40 action?: { url: string; text: string } 41) { 42 return sendEmail({ 43 to, 44 subject: title, 45 react: NotificationEmail({ 46 name, 47 title, 48 message, 49 actionUrl: action?.url, 50 actionText: action?.text, 51 }), 52 }); 53} 54 55export async function sendReceiptEmail( 56 to: string, 57 name: string, 58 receiptData: { 59 receiptNumber: string; 60 date: string; 61 items: Array<{ description: string; amount: string }>; 62 total: string; 63 } 64) { 65 return sendEmail({ 66 to, 67 subject: `Receipt #${receiptData.receiptNumber}`, 68 react: ReceiptEmail({ 69 name, 70 ...receiptData, 71 }), 72 }); 73}

Step 7: Add Email Queue (Optional)#

For high-volume applications, add a queue system:

1// lib/email-queue.ts 2import { prisma } from '@/lib/prisma'; 3import { sendEmail } from '@/lib/email'; 4 5interface QueuedEmail { 6 id: string; 7 to: string; 8 subject: string; 9 templateName: string; 10 templateData: Record<string, unknown>; 11 status: 'pending' | 'sent' | 'failed'; 12 attempts: number; 13 maxAttempts: number; 14 error?: string; 15} 16 17export async function queueEmail( 18 to: string, 19 subject: string, 20 templateName: string, 21 templateData: Record<string, unknown> 22) { 23 return prisma.emailQueue.create({ 24 data: { 25 to, 26 subject, 27 templateName, 28 templateData: templateData as any, 29 status: 'pending', 30 attempts: 0, 31 maxAttempts: 3, 32 }, 33 }); 34} 35 36export async function processEmailQueue() { 37 const emails = await prisma.emailQueue.findMany({ 38 where: { 39 status: 'pending', 40 attempts: { lt: 3 }, 41 }, 42 take: 10, 43 }); 44 45 for (const email of emails) { 46 try { 47 // Dynamically import and render template 48 const template = await getTemplate(email.templateName, email.templateData); 49 50 await sendEmail({ 51 to: email.to, 52 subject: email.subject, 53 react: template, 54 }); 55 56 await prisma.emailQueue.update({ 57 where: { id: email.id }, 58 data: { status: 'sent' }, 59 }); 60 } catch (error) { 61 await prisma.emailQueue.update({ 62 where: { id: email.id }, 63 data: { 64 attempts: { increment: 1 }, 65 status: email.attempts >= 2 ? 'failed' : 'pending', 66 error: String(error), 67 }, 68 }); 69 } 70 } 71} 72 73async function getTemplate(name: string, data: Record<string, unknown>) { 74 const templates: Record<string, (data: any) => React.ReactElement> = { 75 welcome: (d) => WelcomeEmail(d), 76 passwordReset: (d) => PasswordResetEmail(d), 77 notification: (d) => NotificationEmail(d), 78 receipt: (d) => ReceiptEmail(d), 79 }; 80 81 const Template = templates[name]; 82 if (!Template) { 83 throw new Error(`Unknown template: ${name}`); 84 } 85 86 return Template(data); 87}

Step 8: Preview Emails#

Create a preview route for development:

1// app/emails/preview/[template]/page.tsx 2import { WelcomeEmail } from '@/emails/WelcomeEmail'; 3import { PasswordResetEmail } from '@/emails/PasswordResetEmail'; 4import { NotificationEmail } from '@/emails/NotificationEmail'; 5import { ReceiptEmail } from '@/emails/ReceiptEmail'; 6 7const templates = { 8 welcome: () => ( 9 <WelcomeEmail 10 name="John" 11 loginUrl="https://example.com/dashboard" 12 /> 13 ), 14 'password-reset': () => ( 15 <PasswordResetEmail 16 name="John" 17 resetUrl="https://example.com/reset" 18 expiresIn="1 hour" 19 /> 20 ), 21 notification: () => ( 22 <NotificationEmail 23 name="John" 24 title="New Feature Available" 25 message="We've just released a new feature you'll love!" 26 actionUrl="https://example.com/features" 27 actionText="Check it out" 28 /> 29 ), 30 receipt: () => ( 31 <ReceiptEmail 32 name="John" 33 receiptNumber="REC-001" 34 date="March 20, 2024" 35 items={[ 36 { description: 'Pro Plan (Monthly)', amount: '$29.00' }, 37 ]} 38 total="$29.00" 39 /> 40 ), 41}; 42 43export default function PreviewPage({ 44 params, 45}: { 46 params: { template: keyof typeof templates }; 47}) { 48 const Template = templates[params.template]; 49 50 if (!Template) { 51 return <div>Template not found</div>; 52 } 53 54 return <Template />; 55} 56 57export function generateStaticParams() { 58 return Object.keys(templates).map((template) => ({ template })); 59}

Step 9: Integrate with Webhooks#

Send emails when events occur:

1// app/api/webhooks/clerk/route.ts 2import { sendWelcomeEmail } from '@/lib/emails/send'; 3 4// In your webhook handler 5if (eventType === 'user.created') { 6 const { email_addresses, first_name } = evt.data; 7 const email = email_addresses[0]?.email_address; 8 const name = first_name || 'there'; 9 10 await sendWelcomeEmail(email, name); 11}

Verification Checklist#

  • Welcome email sends on signup
  • Email templates render correctly
  • Preview route works in development
  • Resend dashboard shows sent emails

What You Learned#

  • Email provider integration
  • React Email templates
  • Email sending patterns
  • Queue implementation
  • Preview system

Next Steps#