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#
- Go to resend.com and sign up
- Verify your domain (or use sandbox for testing)
- Create an API key
Add Environment Variables#
# .env.local
RESEND_API_KEY=re_...
EMAIL_FROM=noreply@yourdomain.comStep 2: Apply Email Skill#
bootspring skill apply email/resend
bootspring skill apply email/templatesStep 3: Install Dependencies#
npm install resend @react-email/components react-emailStep 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