Email System
Complete guide to the email system implementation using Resend, React Email, and NextAuth.js for authentication and notifications.
The YouTube Analyzer uses a comprehensive email system for user authentication, notifications, and analysis result delivery powered by Resend and React Email.
Overview
The email system handles:
- Magic link authentication for passwordless login
- Account activation for new user registration
- Analysis result notifications when YouTube analysis completes
- Subscription management email updates
- Transactional emails for billing and account changes
Architecture
graph TD
A[Email Trigger] --> B[Email Service]
B --> C[React Email Template]
C --> D[Resend API]
D --> E[Email Delivery]
F[NextAuth.js] --> B
G[Analysis Complete] --> B
H[Subscription Change] --> BConfiguration
Environment Variables
# Resend API Configuration
RESEND_API_KEY=re_your_resend_api_key
# Email sender address
EMAIL_FROM=noreply@yourdomain.com
# Development email override
NODE_ENV=development # Routes all emails to delivered@resend.devNextAuth.js Integration
// auth.config.ts
import Resend from "next-auth/providers/resend";
import { sendVerificationRequest } from "@/lib/email";
export default {
providers: [
Resend({
apiKey: env.RESEND_API_KEY,
from: env.EMAIL_FROM,
sendVerificationRequest, // Custom email handler
}),
],
} satisfies NextAuthConfig;Email Service
Core Email Service (lib/email.ts)
import { MagicLinkEmail } from "@/emails/magic-link-email";
import { Resend } from "resend";
import { getUserByEmail } from "./user";
export const resend = new Resend(env.RESEND_API_KEY);
export const sendVerificationRequest: EmailConfig["sendVerificationRequest"] =
async ({ identifier, url, provider }) => {
const user = await getUserByEmail(identifier);
if (!user || !user.name) return;
const userVerified = user?.emailVerified ? true : false;
const authSubject = userVerified
? `Sign-in link for ${siteConfig.name}`
: "Activate your account";
try {
const { data, error } = await resend.emails.send({
from: provider.from,
to: process.env.NODE_ENV === "development"
? "delivered@resend.dev"
: identifier,
subject: authSubject,
react: MagicLinkEmail({
firstName: user?.name as string,
actionUrl: url,
mailType: userVerified ? "login" : "register",
siteName: siteConfig.name,
}),
headers: {
"X-Entity-Ref-ID": new Date().getTime() + "",
},
});
if (error || !data) {
throw new Error(error?.message);
}
} catch (error) {
throw new Error("Failed to send verification email.");
}
};Analysis Result Email Service
// lib/email.ts (extended)
export async function sendAnalysisResultEmail(
userId: string,
analysisId: string,
channelName: string,
videoCount: number
) {
const user = await getUserById(userId);
if (!user?.email) return;
try {
const { data, error } = await resend.emails.send({
from: env.EMAIL_FROM,
to: user.email,
subject: `Your YouTube Analysis is Ready - ${channelName}`,
react: AnalysisResultEmail({
userName: user.name || "there",
channelName,
videoCount,
analysisUrl: `${env.NEXT_PUBLIC_APP_URL}/dashboard/results/${analysisId}`,
dashboardUrl: `${env.NEXT_PUBLIC_APP_URL}/dashboard`,
}),
});
if (error) {
console.error("Failed to send analysis result email:", error);
}
} catch (error) {
console.error("Email sending error:", error);
}
}Email Templates
Magic Link Email Template
// emails/magic-link-email.tsx
import {
Body, Button, Container, Head, Hr, Html,
Preview, Section, Tailwind, Text,
} from "@react-email/components";
import { Icons } from "../components/shared/icons";
type MagicLinkEmailProps = {
actionUrl: string;
firstName: string;
mailType: "login" | "register";
siteName: string;
};
export const MagicLinkEmail = ({
firstName = "",
actionUrl,
mailType,
siteName,
}: MagicLinkEmailProps) => (
<Html>
<Head />
<Preview>
{mailType === "login"
? `Sign in to ${siteName}`
: `Welcome to ${siteName}! Activate your account.`}
</Preview>
<Tailwind>
<Body className="bg-white font-sans">
<Container className="mx-auto py-5 pb-12">
<Icons.logo className="m-auto block size-10" />
<Text className="text-base">Hi {firstName},</Text>
<Text className="text-base">
Welcome to {siteName}! Click the link below to{" "}
{mailType === "login" ? "sign in to" : "activate"} your account.
</Text>
<Section className="my-5 text-center">
<Button
className="inline-block rounded-md bg-zinc-900 px-4 py-2 text-base text-white no-underline"
href={actionUrl}
>
{mailType === "login" ? "Sign in" : "Activate Account"}
</Button>
</Section>
<Text className="text-base">
This link expires in 24 hours and can only be used once.
</Text>
{mailType === "login" ? (
<Text className="text-base">
If you did not try to log into your account, you can safely ignore it.
</Text>
) : null}
<Hr className="my-4 border-t-2 border-gray-300" />
<Text className="text-sm text-gray-600">
123 Code Street, Suite 404, Devtown, CA 98765
</Text>
</Container>
</Body>
</Tailwind>
</Html>
);Analysis Result Email Template
// emails/analysis-result-email.tsx
import {
Body, Button, Container, Head, Hr, Html,
Preview, Section, Tailwind, Text, Link,
} from "@react-email/components";
import { Icons } from "../components/shared/icons";
type AnalysisResultEmailProps = {
userName: string;
channelName: string;
videoCount: number;
analysisUrl: string;
dashboardUrl: string;
};
export const AnalysisResultEmail = ({
userName,
channelName,
videoCount,
analysisUrl,
dashboardUrl,
}: AnalysisResultEmailProps) => (
<Html>
<Head />
<Preview>
Your YouTube analysis for {channelName} is ready! View {videoCount} video insights.
</Preview>
<Tailwind>
<Body className="bg-white font-sans">
<Container className="mx-auto py-5 pb-12">
<Icons.logo className="m-auto block size-10" />
<Text className="text-base">Hi {userName},</Text>
<Text className="text-base">
Great news! Your YouTube analysis for <strong>{channelName}</strong> is now complete.
</Text>
<Text className="text-base">
We've analyzed <strong>{videoCount} videos</strong> and generated comprehensive insights including:
</Text>
<ul className="ml-4 list-disc">
<li>Content themes and trends</li>
<li>Performance metrics analysis</li>
<li>Engagement patterns</li>
<li>Audience insights</li>
<li>Optimization recommendations</li>
</ul>
<Section className="my-5 text-center">
<Button
className="inline-block rounded-md bg-zinc-900 px-4 py-2 text-base text-white no-underline"
href={analysisUrl}
>
View Analysis Results
</Button>
</Section>
<Text className="text-base">
You can also access all your analyses from your{" "}
<Link href={dashboardUrl} className="text-blue-600 underline">
dashboard
</Link>.
</Text>
<Hr className="my-4 border-t-2 border-gray-300" />
<Text className="text-sm text-gray-600">
YouTube Analyzer - Unlock insights from any YouTube channel
</Text>
</Container>
</Body>
</Tailwind>
</Html>
);Email Triggers
Authentication Flow
The email system is automatically triggered during authentication:
// When user attempts to sign in
const result = await signIn("resend", {
email: userEmail,
callbackUrl: "/dashboard",
redirect: false,
});
// This triggers sendVerificationRequest in auth.config.ts
// Which sends magic link email via lib/email.tsAnalysis Completion
// After analysis completes in API route
export async function POST(request: Request) {
// ... analysis processing ...
// Send completion email
await sendAnalysisResultEmail(
userId,
analysis.id,
channelData.snippet.title,
videoCount
);
return Response.json({ status: "complete" });
}Subscription Changes
// Stripe webhook handler
export async function POST(req: Request) {
const event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
if (event.type === "customer.subscription.created") {
// Send welcome email
await sendSubscriptionWelcomeEmail(user.email, subscription);
}
if (event.type === "customer.subscription.updated") {
// Send plan change confirmation
await sendSubscriptionUpdateEmail(user.email, subscription);
}
}Development Workflow
Local Development
In development mode, all emails are redirected to delivered@resend.dev to prevent accidental sending:
const recipient = process.env.NODE_ENV === "development"
? "delivered@resend.dev"
: actualEmailAddress;Email Testing
- Resend Dashboard: View sent emails in the Resend dashboard
- React Email Preview: Use
npm run emailto preview templates - Development Emails: All dev emails go to
delivered@resend.dev
# Preview email templates locally
npm run email
# This starts React Email preview server
# Access at http://localhost:3000Template Development
// Create new email template
export const NewEmailTemplate = (props: NewEmailProps) => (
<Html>
<Head />
<Preview>Email preview text</Preview>
<Tailwind>
<Body>
{/* Email content using Tailwind classes */}
</Body>
</Tailwind>
</Html>
);Email Delivery Features
Headers and Threading
headers: {
// Prevent Gmail threading
"X-Entity-Ref-ID": new Date().getTime() + "",
// Custom tracking headers
"X-Email-Type": "magic-link",
"X-User-ID": userId,
}Unsubscribe Handling
// Add unsubscribe link to marketing emails
headers: {
"List-Unsubscribe": `<${unsubscribeUrl}>`,
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click",
}Email Analytics
// Track email opens and clicks
const trackingPixel = `${env.NEXT_PUBLIC_APP_URL}/api/email/track?email=${emailId}&event=open`;
// In email template
<img src={trackingPixel} width="1" height="1" style={{ display: 'none' }} />Error Handling
Retry Logic
async function sendEmailWithRetry(emailData: EmailData, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const result = await resend.emails.send(emailData);
return result;
} catch (error) {
if (attempt === maxRetries) {
// Log final failure
console.error(`Email failed after ${maxRetries} attempts:`, error);
throw error;
}
// Wait before retry
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
}
}
}Validation
const emailSchema = z.object({
to: z.string().email(),
subject: z.string().min(1),
html: z.string().optional(),
react: z.any().optional(),
});
export async function sendEmail(data: z.infer<typeof emailSchema>) {
const validated = emailSchema.parse(data);
return await resend.emails.send(validated);
}Security Considerations
Email Verification
- Magic links expire after 24 hours
- Links can only be used once
- Email addresses are verified before account activation
Rate Limiting
// Implement rate limiting for email sending
const emailRateLimit = new Ratelimit({
redis: kv,
limiter: Ratelimit.slidingWindow(5, "1 h"), // 5 emails per hour
});
export async function sendRateLimitedEmail(email: string, emailData: EmailData) {
const { success } = await emailRateLimit.limit(email);
if (!success) {
throw new Error("Email rate limit exceeded");
}
return await resend.emails.send(emailData);
}Content Security
// Sanitize email content
import DOMPurify from 'isomorphic-dompurify';
const sanitizedContent = DOMPurify.sanitize(userContent);Monitoring & Analytics
Email Metrics
- Delivery Rate: Percentage of emails successfully delivered
- Open Rate: Percentage of emails opened by recipients
- Click Rate: Percentage of email links clicked
- Bounce Rate: Percentage of emails that bounced
Logging
// Email event logging
await prisma.emailLog.create({
data: {
userId,
emailType: "magic-link",
recipient: email,
status: "sent",
resendId: result.id,
sentAt: new Date(),
},
});Troubleshooting
Common Issues
- Emails not sending: Check Resend API key and sender domain verification
- Templates not rendering: Verify React Email component syntax
- Development email issues: Ensure
delivered@resend.devis receiving emails - Production delivery: Verify sender domain DNS records
Debug Commands
# Test email template rendering
npm run email
# Check Resend API status
curl -H "Authorization: Bearer re_your_api_key" https://api.resend.com/emails
# Verify domain configuration
dig TXT your-domain.comAuto-Analysis Email Notification System
The auto-analysis notification system batches analysis outputs into either Daily or Weekly digest emails (no more per-analysis immediate sends) to reduce noise and improve deliverability. Manual "Run Now" analyses are intentionally excluded from digests.
Architecture Overview
graph TD
A[Auto-Analysis Cron Job] --> B{Check Notification Settings}
B -->|Daily| C[Queue for Daily Digest]
B -->|Weekly| D[Queue for Weekly Digest]
B -->|Disabled| E[No Notification]
F[Daily Digest Cron] --> C2[Process Daily Digests]
G[Weekly Digest Cron] --> H[Process Weekly Digests]
H --> I[Send Unified Digest]Notification Preferences
- Global / Per-Subscription
notificationFrequency:daily|weekly|none- (Deprecated)
combineAllResults: Ignored; all digests are unified now.
- Deprecation
- Legacy
immediatemigrated todaily(script + runtime guard) combineAllResultssoft-deprecated (accepted, ignored, will be removed)
Data Model (Excerpt)
model AutoAnalysis {
// ... existing fields ...
emailNotificationsEnabled Boolean @default(true)
notificationFrequency String @default("daily")
notificationDayOfWeek String @default("monday")
combineAllResults Boolean @default(false) // DEPRECATED: unified digests
}Core Flow
On analysis completion we enqueue for the appropriate digest (no immediate send):
const result = await sendAnalysisNotification({ analysisId, autoAnalysisId });
// result.reason will be 'queued_for_daily' | 'queued_for_weekly'Daily job calls processDailyDigests() (24h lookback). Weekly job calls processWeeklyDigests(day) (7d lookback). Queries exclude triggerType === 'manual'.
Email Rendering
Unified HTML assembly builds a single email per cadence. Subjects simplified:
Daily Digest
Weekly Digest
Counts + sources rendered in body (removed from subject for brevity / deliverability). Topic analyses use condensed blocks; channels retain structured sections.
Cron Integration
Unified digests: POST /api/cron/digests (protected by x-cron-secret) – processes both daily (24h window) and weekly (if today matches user weekly day) in one call.
Manual Runs
Manual dashboard "Run Now" analyses are excluded (noise reduction and to prevent duplicate visibility). Only scheduled (automated) analyses are digested.
Migration Steps
- Deploy code with dual support (runtime normalization of
immediate) - Run
tsx scripts/migrate-immediate-to-daily.ts - Remove any stale references (docs/scripts) – complete in this change set.
Deduplication & Credit Integrity
Early in /api/youtube/process multi-video submissions:
- Detect duplicate YouTube video IDs already analyzed by the same user
- If all duplicates → return 409, no credits reserved
- Filter duplicates, recalc cost, then reserve credits for remaining unique videos
- Proceed with processing & final credit settlement
Ensures no double-charging and accurate reservedCredits usage.
Future Enhancements
- Idempotency metadata (
lastDailyDigestSentAt) for double-send protection - Quiet hours / user timezone batching
- In-app notification center integration
- Final schema migration removing
combineAllResults
Logging
[AutoAnalysis Cron] Queued analysis <id> for digest processing
[Daily Digest Cron] Sent X of Y daily digests
[Weekly Digest Cron] Sent X of Y weekly digests
Legacy log lines mentioning "Sending immediate" have been removed.
API Endpoints for User Preferences
Notification settings are managed through the auto-analysis API endpoints:
- Create with notification settings:
POST /api/auto-analysis - Update notification settings:
PATCH /api/auto-analysis/[id]
Example payload for updating notification settings (combineAllResults omitted / ignored):
{
"emailNotificationsEnabled": true,
"notificationFrequency": "weekly",
"notificationDayOfWeek": "monday",
// combineAllResults deprecated – always unified
}Testing the Notification System
Several test scripts are available for validating the notification system:
- Manual testing script:
scripts/test-notification-system.ts - Simplified notification test:
scripts/simple-notification-test.ts - End-to-end test:
scripts/e2e-notification-test.ts
Example test command:
npx tsx scripts/test-notification-system.tsTroubleshooting
Common issues with the notification system:
- No emails being sent: Check
emailNotificationsEnabledandnotificationFrequencyin the database - Immediate emails working but no weekly digests: Verify the weekly cron job is running and check logs
Operations: Notification Jobs & One-Off Runs
These operational commands are not exposed as npm scripts to keep package.json lean. Run them manually when needed.
Data Normalization (Legacy immediate -> daily)
npx ts-node scripts/migrate-immediate-to-daily.tsSafe to re-run; updates any remaining legacy rows.
Force Daily Digest Send (Dev/Test)
npx ts-node scripts/run-daily-digest-once.tsProduces a JSON summary; in dev will send to delivered@resend.dev.
Force Weekly Digest Logic (Any Day)
npx ts-node -e "import('./lib/cron/weekly-digest').then(m=>m.runWeeklyDigestCron())"Invoke API Cron Endpoints (Post-Deploy)
curl -X POST -H "x-cron-secret: $CRON_SECRET" https://<your-domain>/api/cron/digestsEnsure CRON_SECRET matches the environment variable in deployment.
- Wrong day of week: Ensure
notificationDayOfWeekmatches the cron schedule
Debug logging:
console.log(`[AutoAnalysis Cron] Notification result:`, notificationResult);
console.log(`[Weekly Digest Cron] Processed ${results.length} weekly digests`);Environment Configuration
The notification system uses the same email configuration as the main email system, with development emails sent to delivered@resend.dev:
to: process.env.NODE_ENV === "development" ? "delivered@resend.dev" : user.emailFuture Enhancements
- Complete combined digest template: The current implementation needs a custom template for combined digests
- In-app notifications: Integration with an in-app notification center
- Unsubscribe links: Per-subscription and global unsubscribe options
Production Monitoring
The notification system includes comprehensive logging for production monitoring:
console.log(`[AutoAnalysis Cron] Sending immediate notification for analysis ${processData.id}`);
console.log(`[Weekly Digest Cron] Processed ${results.length} weekly digests`);These logs can be used to track the performance and reliability of the notification system in production.
The email system provides a robust foundation for all user communications, from authentication to result delivery, ensuring reliable and professional email delivery throughout the application.