Email functionality for transactional emails, password resets, and notifications.
Overview
ShipSafe uses Resend natively for all email sending. Resend is integrated directly into the boilerplate, providing a modern, developer-friendly email API for transactional emails.
Key Features:
- Native Resend integration - Built-in email service
- Email templates - Pre-built templates (welcome, reset, invite)
- Type-safe API - Full TypeScript support
- Email enumeration prevention - Security best practices
- Server-side only - Secure email sending
Current Implementation
Resend Email Service
ShipSafe uses Resend natively for all email sending. The email service is located in src/lib/email/.
File Structure:
The email service is organized as follows:
src/lib/email/
send_email.ts- Main email sending functiontemplates/- Email template componentswelcome.tsx- Welcome email templatereset.tsx- Password reset templateinvite.tsx- Invitation email templateindex.ts- Template exports
Main File: src/lib/email/send_email.ts
import { sendEmail } from "@/lib/email/send_email";
// Send email
const result = await sendEmail({
to: "user@example.com",
subject: "Welcome!",
html: "<h1>Welcome!</h1>",
text: "Welcome!",
});
Setup
Install Resend
First, install the Resend package:
npm install resend
Get Resend API Key
- Sign up for Resend - Go to resend.com
- Create API key - Navigate to API Keys section
- Copy your API key - It starts with
re_
Environment Variables
Add your Resend API key to .env.local:
# Resend API Key
RESEND_API_KEY=re_xxxxxxxxxxxxx
# Domain for email links (optional, uses config.ts by default)
NEXT_PUBLIC_APP_URL=https://yourdomain.com
Note: The from email address will default to ShipSafe <no-reply@${config.domainName}> but can be customized.
API Integration
Password Reset Endpoint
Location: src/app/api/auth/reset/route.ts
import { NextRequest, NextResponse } from "next/server";
import { sendPasswordResetSchema } from "@/features/auth/schema";
import { sendPasswordResetEmail } from "@/features/auth/send-reset";
export async function POST(req: NextRequest) {
try {
const body = await req.json();
const parsed = sendPasswordResetSchema.parse(body);
// Send password reset email
const result = await sendPasswordResetEmail(parsed.email);
// Always return success to prevent email enumeration
return NextResponse.json({
success: true,
message: result.message,
});
} catch (error) {
// Error handling...
}
}
Security
Email Enumeration Prevention
Important: Always return the same success message regardless of whether the email exists. This prevents attackers from discovering valid email addresses.
// ✅ Good: Always returns same message
return {
success: true,
message: "If an account exists, a password reset email has been sent.",
};
// ❌ Bad: Reveals if email exists
if (!userExists) {
return { error: "Email not found" }; // DON'T DO THIS
}
Rate Limiting
Password reset endpoints should be rate-limited to prevent abuse:
// Middleware applies rate limiting automatically
// See: src/lib/security/rate_limit.ts
Using Email Templates
ShipSafe includes pre-built email templates that you can use:
Available Templates
Location: src/lib/email/templates/
- welcome.tsx - Welcome email for new signups
- reset.tsx - Password reset email
- invite.tsx - User invitation email
Using Templates
import { sendEmailWithTemplate } from "@/lib/email/send_email";
import { welcomeEmailTemplate } from "@/lib/email/templates";
// Send welcome email
const result = await sendEmailWithTemplate(
welcomeEmailTemplate,
"user@example.com",
{ userName: "John", dashboardUrl: "https://yourdomain.com/dashboard" }
);
Template Functions
All templates return { subject, html, text }:
// Welcome email
welcomeEmailTemplate({ userName: "John", dashboardUrl?: string })
// Password reset
resetEmailTemplate({ resetLink: string, userName?: string })
// Invitation
inviteEmailTemplate({
inviterName: string,
inviteLink: string,
customMessage?: string,
roleOrTeam?: string
})
Common Use Cases
Welcome Email
import { sendEmailWithTemplate } from "@/lib/email/send_email";
import { welcomeEmailTemplate } from "@/lib/email/templates";
async function sendWelcomeEmail(user: User) {
const result = await sendEmailWithTemplate(
welcomeEmailTemplate,
user.email,
{
userName: user.displayName || "User",
dashboardUrl: `https://${config.domainName}/dashboard`,
}
);
if (!result.success) {
console.error("Failed to send welcome email:", result.error);
}
}
Password Reset Email
import { sendEmailWithTemplate } from "@/lib/email/send_email";
import { resetEmailTemplate } from "@/lib/email/templates";
import { getAuth } from "firebase-admin/auth";
import { getAdminApp } from "@/lib/firebase/init";
async function sendPasswordResetEmail(email: string) {
// Generate reset link
const adminAuth = getAuth(getAdminApp());
const resetLink = await adminAuth.generatePasswordResetLink(email, {
url: `https://${config.domainName}/auth/reset?oobCode=`,
handleCodeInApp: false,
});
// Send email via Resend
const result = await sendEmailWithTemplate(
resetEmailTemplate,
email,
{ resetLink }
);
return {
success: result.success,
message: "If an account exists, a password reset email has been sent.",
};
}
Notification Emails
async function sendNotificationEmail(user: User, message: string) {
await sendEmail({
to: user.email,
subject: "New Notification",
html: `<p>${message}</p>`,
});
}
Subscription Updates
async function sendSubscriptionConfirmationEmail(user: User, plan: string) {
await sendEmail({
to: user.email,
subject: "Subscription Confirmed",
html: `
<h1>Subscription Confirmed</h1>
<p>Your ${plan} subscription is now active.</p>
`,
});
}
Email Service
Resend (Native Integration)
ShipSafe uses Resend natively - it's already integrated and ready to use.
Why Resend:
- ✅ Simple, modern API
- ✅ Excellent developer experience
- ✅ Reasonable pricing (free tier available)
- ✅ Great deliverability
- ✅ Built-in analytics
Already Set Up:
- ✅ Resend client initialized in
src/lib/email/send_email.ts - ✅ Email templates ready to use
- ✅ Type-safe API with TypeScript
- ✅ Error handling built-in
SendGrid
Pros:
- Enterprise features
- High deliverability
- Analytics dashboard
Setup:
npm install @sendgrid/mail
import sgMail from "@sendgrid/mail";
sgMail.setApiKey(process.env.SENDGRID_API_KEY!);
Best Practices
- Email enumeration prevention - Always return same message
- Rate limiting - Limit email sending per user/IP
- Email validation - Validate email format before sending
- Error handling - Handle email service failures gracefully
- Templates - Use templates for consistent branding
- Unsubscribe - Include unsubscribe links for marketing emails
- Testing - Use email testing services in development
- Logging - Log email sends for debugging
Testing
Development
Use email testing services:
- Mailtrap - Test email server
- MailHog - Local email testing
- Ethereal Email - Temporary email addresses
Testing Email Sends
// In development, log emails instead of sending
if (process.env.NODE_ENV === "development") {
console.log("Email would be sent:", { to, subject });
return;
}
// Production: actually send email
await sendEmail({ to, subject, html });
Troubleshooting
Email Not Received
- Check spam folder - Emails may be filtered
- Verify email service - Check service status
- Check API key - Ensure API key is valid
- Verify sender email - Must be verified in email service
- Check logs - Review server logs for errors
Firebase Auth Email Issues
- Check Firebase Console - Verify email template is configured
- Verify domain - Ensure domain is authorized
- Check action URL - Verify redirect URL is correct
- Review Firebase logs - Check for email delivery issues
Learn More
- Firebase Auth Email Templates
- Authentication Features - Auth and password reset
- Environment Variables - Email service configuration