Skip to main content

Chapter 5: Email System

In the previous chapter, we explored how ShipNowKit's Template System helps you build complete pages and interfaces. Now, let's turn our attention to how your application communicates with users outside your website through the Email System.

Introduction

Imagine you're running a physical store. When customers sign up for your loyalty program, you need a way to send them their membership cards. You might hire a postal service to deliver these cards to their homes.

Your web application faces a similar challenge - it needs to communicate with users even when they're not actively using your site. The Email System is like your application's postal service, formatting messages and delivering them directly to your users' inboxes.

The Problem: Reliable User Communication

Without a proper email system, communicating with users becomes challenging:

  • You need to design professional-looking emails that reflect your brand
  • Sending emails requires integrating with email delivery services
  • You need to handle different types of emails (authentication, notifications, etc.)
  • Tracking delivery status and handling errors gets complicated

ShipNowKit's Email System Solution

ShipNowKit solves these problems with an integrated Email System that:

  1. Uses beautiful, responsive email templates
  2. Integrates with Resend (a popular email delivery service)
  3. Makes sending emails as simple as calling a function
  4. Maintains consistent branding across all communications

The primary use case in ShipNowKit is sending magic links for passwordless authentication, but the system is designed to be extensible for other types of notifications.

Let's see how it works!

Key Components of the Email System

The Email System has several important parts that work together:

1. Email Templates

Email templates define how your emails look. They're built using React components from the @react-email/components library, which makes creating beautiful emails as easy as building React components.

Here's an example of a magic link email template:

// emailTemplate/magicLinks/NotionLike.tsx (simplified)
import {
Body, Container, Head, Heading,
Html, Link, Preview, Text
} from "@react-email/components";
import { siteConfig } from "@/config";

export const NotionMagicLinkEmail = ({ magicLink }) => (
<Html>
<Head />
<Preview>Log in with this magic link</Preview>
<Body style={main}>
<Container style={container}>
<Heading style={h1}>Login</Heading>
<Link href={magicLink} target="_blank" style={link}>
Click here to log in with this magic link
</Link>
<Text style={text}>
If you didn't try to login, you can safely ignore this email.
</Text>
<img src={siteConfig.icons.logo} width="32" height="32" alt="Logo" />
</Container>
</Body>
</Html>
);

// Style definitions omitted for brevity

This template creates a clean, simple email with your site's branding that contains a magic link button. When the user clicks this link, they'll be signed in to your application.

Notice how the template uses components like Html, Body, and Link - similar to how you'd build a web page, but optimized for email clients.

2. Template Types

To ensure type safety, each email template type has its own interface:

// emailTemplate/magicLinks/types.ts
export type MagicLinkEmailProps = {
magicLink: string;
}

This interface defines what properties the magic link email template expects - in this case, just the magicLink URL that the user will click to sign in.

3. Template Registry

The template registry keeps track of all available email templates:

// emailTemplate/magicLinks/registry.ts
import * as React from 'react';
import { NotionMagicLinkEmail } from "./NotionLike";
import { MagicLinkEmailProps } from "./types";

export const templateRegistry = new Map<string,
(props: MagicLinkEmailProps) => React.ReactNode>();

function registerTemplate(name: string,
template: (props: MagicLinkEmailProps) => React.ReactNode) {
templateRegistry.set(name.toLowerCase(), template);
}

registerTemplate('NotionLike', NotionMagicLinkEmail);

This registry uses a Map to store all available template functions, with their names as keys. The registerTemplate function adds new templates to this registry.

Currently, only the "NotionLike" template is registered, but you could add more templates with different designs.

4. Template Loader

The template loader selects and loads the appropriate template:

// emailTemplate/magicLinks/index.ts
import { logger } from "@/lib/logger";
import { templateRegistry } from "./registry";
import { MagicLinkEmailProps } from "./types";

export async function getMagicLinkEmailTemplate(props: MagicLinkEmailProps) {
try {
// Get template name from environment variable or use default
const templateName = (process.env.MAGIC_LINK_TEMPLATE || 'NotionLike').toLowerCase();

// Get the template from the registry
let template = templateRegistry.get(templateName);

// Fall back to default if not found
if (!template) {
logger.warn(`Template "${templateName}" not found, using default`);
template = templateRegistry.get('notionlike');
if (!template) {
throw new Error('Default template not found');
}
}

// Return the template with the provided props
return template(props);
} catch (error) {
logger.error('Error loading email template:', error);
throw new Error(`Failed to load email template: ${error}`);
}
}

This function:

  1. Gets the template name from an environment variable (or uses "NotionLike" by default)
  2. Retrieves the template function from the registry
  3. Falls back to a default template if the requested one isn't found
  4. Returns the rendered template with the provided props

Using the Email System for Authentication

Now let's see how to use the Email System to send authentication magic links:

// lib/actions/auth.ts (simplified)
'use server'

import { signIn } from "@/auth";
import { authConfig } from "@/config";
import { getMagicLinkEmailTemplate } from "@/emailTemplate/magicLinks";
import { Resend } from 'resend';

export async function sendMagicLinkAction(formData: FormData) {
const email = formData.get("email") as string;

// Validate the email (simplified)
if (!email || !email.includes('@')) {
return { error: "Invalid email address" };
}

try {
// This will trigger the verification request function
await signIn("email", {
email: email,
redirectTo: authConfig.defaultAuthCallbackUrl,
redirect: false,
});

return { success: "Magic link sent! Check your email." };
} catch (error) {
return { error: "Failed to send magic link. Please try again." };
}
}

// This function is called by NextAuth when sending magic links
export async function resendVerificationRequest(params) {
const { identifier: to, url } = params
const { host } = new URL(url)

// Initialize the Resend client with your API key
const resend = new Resend(process.env.RESEND_API_KEY!)

try {
// Send the email
const { data, error } = await resend.emails.send({
from: process.env.AUTH_EMAIL_FROM!,
to: [to],
subject: `Sign in to ${host}`,
react: getMagicLinkEmailTemplate({ magicLink: url }),
});

if (error) {
return Response.json({ error }, { status: 500 });
}

return Response.json(data);
} catch (error) {
return Response.json({ error }, { status: 500 });
}
}

This code shows two key functions:

  1. sendMagicLinkAction: A server action that initiates the magic link flow when a user submits the sign-in form
  2. resendVerificationRequest: Handles the actual email sending via Resend

The flow works like this:

  • User enters their email address and submits the form
  • sendMagicLinkAction validates the email and calls signIn("email", ...)
  • The authentication system calls resendVerificationRequest with the user's email and a verification URL
  • resendVerificationRequest gets the appropriate email template and sends it via Resend
  • The user receives the email and clicks the link to sign in

How the Email System Works Under the Hood

Let's visualize how the Email System works behind the scenes:

When a user requests a magic link:

  1. Your app calls the authentication system
  2. The authentication system calls the Email System
  3. The Email System loads the appropriate template
  4. The email is sent via Resend
  5. The user receives the email and clicks the link
  6. They're automatically signed in

Customizing the Email System

Creating Your Own Email Template

You can create custom email templates to match your brand:

// emailTemplate/magicLinks/BrandedTemplate.tsx
import {
Body, Container, Head, Heading,
Html, Link, Preview, Text
} from "@react-email/components";
import { siteConfig } from "@/config";
import { MagicLinkEmailProps } from "./types";

export const BrandedMagicLinkEmail = ({ magicLink }: MagicLinkEmailProps) => (
<Html>
<Head />
<Preview>Your login link for {siteConfig.name}</Preview>
<Body style={{ backgroundColor: "#f5f5f5" }}>
<Container style={{ padding: "20px", maxWidth: "600px" }}>
{/* Your company logo */}
<img src={siteConfig.icons.logo} width="60" height="60" alt="Logo" />

<Heading style={{ color: "#4a4a4a", marginTop: "30px" }}>
Welcome to {siteConfig.name}!
</Heading>

<Text style={{ color: "#4a4a4a", fontSize: "16px" }}>
Click the button below to sign in to your account.
This link will expire in 15 minutes.
</Text>

<Link
href={magicLink}
style={{
backgroundColor: "#0070f3",
color: "white",
padding: "12px 20px",
borderRadius: "4px",
textDecoration: "none",
display: "inline-block",
margin: "20px 0"
}}
>
Sign in to {siteConfig.name}
</Link>

<Text style={{ color: "#8c8c8c", fontSize: "14px" }}>
If you didn't request this email, you can safely ignore it.
</Text>
</Container>
</Body>
</Html>
);

Registering Your Template

Once you've created your custom template, you need to register it:

// In emailTemplate/magicLinks/registry.ts
import { BrandedMagicLinkEmail } from "./BrandedTemplate";

// Add this after the existing registerTemplate call
registerTemplate('Branded', BrandedMagicLinkEmail);

Using Your Template

To use your custom template, set the MAGIC_LINK_TEMPLATE environment variable:

# In your .env file
MAGIC_LINK_TEMPLATE=Branded

Now your custom template will be used when sending magic link emails.

Extending the Email System for Other Types of Emails

The Email System can be extended to handle other types of emails. Here's how you might set up a system for welcome emails:

  1. Create a new directory for welcome emails:
emailTemplate/
├── magicLinks/
└── welcome/
├── types.ts
├── NotionLike.tsx
├── registry.ts
└── index.ts
  1. Define the props type:
// emailTemplate/welcome/types.ts
export type WelcomeEmailProps = {
userName: string;
dashboardUrl: string;
}
  1. Create the template:
// emailTemplate/welcome/NotionLike.tsx
import { Body, Container, /* other imports */ } from "@react-email/components";
import { siteConfig } from "@/config";
import { WelcomeEmailProps } from "./types";

export const NotionWelcomeEmail = ({ userName, dashboardUrl }: WelcomeEmailProps) => (
<Html>
{/* Email content */}
<Body style={main}>
<Container>
<Heading>Welcome to {siteConfig.name}, {userName}!</Heading>
<Text>We're excited to have you on board.</Text>
<Link href={dashboardUrl}>Go to your dashboard</Link>
</Container>
</Body>
</Html>
);
  1. Set up the registry and loader similarly to the magic link system

  2. Create a function to send welcome emails:

// lib/actions/email.ts
'use server'

import { getWelcomeEmailTemplate } from "@/emailTemplate/welcome";
import { Resend } from 'resend';

export async function sendWelcomeEmail(email: string, userName: string) {
const resend = new Resend(process.env.RESEND_API_KEY!);
const dashboardUrl = `${process.env.NEXT_PUBLIC_APP_URL}/dashboard`;

try {
const { data, error } = await resend.emails.send({
from: process.env.AUTH_EMAIL_FROM!,
to: [email],
subject: `Welcome to ${process.env.NEXT_PUBLIC_APP_NAME}!`,
react: await getWelcomeEmailTemplate({
userName,
dashboardUrl
}),
});

if (error) {
console.error("Email error:", error);
return { success: false, error: "Failed to send welcome email" };
}

return { success: true };
} catch (error) {
console.error("Email error:", error);
return { success: false, error: "Failed to send welcome email" };
}
}

Conclusion

ShipNowKit's Email System provides a reliable and customizable way to communicate with your users via email. By using React-based templates, the system ensures consistent branding across all communications while making it easy to create and maintain email designs.

The system integrates with Resend for reliable delivery and provides a simple interface for sending different types of emails. While the primary use case is sending authentication magic links, the architecture makes it easy to extend for welcome emails, notifications, or any other email communications your application needs.

In the next chapter, we'll explore the Authentication System, which works hand-in-hand with the Email System to provide secure, passwordless authentication for your users.