Email System
OneLibro uses Resend for transactional email delivery with React Email templates for beautiful, responsive emails.
Overview
The email system handles:
- Welcome emails for new users
- Invite code delivery
- Password reset notifications
- Budget alert notifications
- Plaid item error notifications
- Invite request confirmations
Architecture
┌─────────────────────────────────────────────────────────────┐
│ Email System Flow │
└─────────────────────────────────────────────────────────────┘
Trigger Event (signup, budget exceeded, etc.)
│
├─> lib/email.ts: sendEmail()
│ │
│ ├─> Select template from emails/templates/
│ ├─> Render React component to HTML
│ ├─> Send via Resend API
│ └─> Log to email_logs table
│
└─> Database: email_logs
│
└─> Track: delivery, opens, clicks, bounces
Email Templates
All email templates are located in emails/templates/ and built with React Email components.
Available Templates
| Template | File | Trigger | Purpose |
|---|---|---|---|
| Welcome Email | WelcomeEmail.tsx | User signup | Welcome new users to OneLibro |
| Invite Code | InviteCodeEmail.tsx | Admin sends invite | Deliver invite code to new user |
| Account Created | AccountCreatedEmail.tsx | Signup complete | Confirm account creation |
| Password Reset | PasswordResetEmail.tsx | Password reset request | Send reset link |
| Budget Alert | BudgetAlertEmail.tsx | Budget threshold exceeded | Notify user of overspending |
| Plaid Item Error | PlaidItemErrorEmail.tsx | Plaid connection fails | Alert user to reconnect account |
| Invite Request Confirmation | InviteRequestConfirmationEmail.tsx | User requests invite | Confirm request received |
Template Structure
Each template uses shared components from emails/components/:
import { EmailLayout } from '../components/EmailLayout';
import { Button } from '../components/Button';
export default function WelcomeEmail({ name }: { name: string }) {
return (
<EmailLayout>
<h1>Welcome to OneLibro, {name}!</h1>
<p>We're excited to have you on board.</p>
<Button href="https://finance.yatheeshnagella.com/dashboard">
Go to Dashboard
</Button>
</EmailLayout>
);
}
Sending Emails
Using the sendEmail Helper
import { sendEmail } from '@/lib/email';
// Send a welcome email
await sendEmail({
to: 'user@example.com',
subject: 'Welcome to OneLibro',
templateKey: 'welcome_email',
templateProps: { name: 'John Doe' },
category: 'transactional',
});
sendEmail Options
interface SendEmailOptions {
to: string; // Recipient email
subject: string; // Email subject
templateKey: string; // Template key from email_templates table
templateProps: Record<string, any>; // Props to pass to template
category?: 'transactional' | 'marketing' | 'system';
replyTo?: string; // Optional reply-to address
}
Email Categories
- transactional: User-triggered emails (signup, password reset)
- marketing: Promotional emails and campaigns
- system: System notifications (alerts, errors)
Database Schema
email_templates Table
Stores email template metadata and configuration.
CREATE TABLE email_templates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
template_key TEXT UNIQUE NOT NULL, -- 'welcome_email', 'budget_alert', etc.
template_name TEXT NOT NULL, -- Human-readable name
description TEXT,
subject_template TEXT NOT NULL, -- Subject line template
is_active BOOLEAN DEFAULT true, -- Can be disabled without deleting
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
email_logs Table
Tracks all sent emails for debugging and analytics.
CREATE TABLE email_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
email_to TEXT NOT NULL,
template_key TEXT,
subject TEXT NOT NULL,
status TEXT NOT NULL, -- 'sent', 'failed', 'bounced'
error_message TEXT,
resend_id TEXT, -- Resend API message ID
category TEXT,
sent_at TIMESTAMPTZ DEFAULT now()
);
budget_alert_history Table
Tracks budget alerts to prevent duplicate notifications.
CREATE TABLE budget_alert_history (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
budget_id UUID REFERENCES budgets(id),
user_id UUID REFERENCES users(id),
alert_type TEXT NOT NULL, -- 'warning', 'exceeded'
amount_spent INTEGER NOT NULL, -- Amount in cents
budget_limit INTEGER NOT NULL, -- Limit in cents
sent_at TIMESTAMPTZ DEFAULT now()
);
Environment Configuration
Required environment variables in .env:
# Resend API key
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxx
# Email sender configuration
NEXT_PUBLIC_FROM_EMAIL=noreply@yatheeshnagella.com
NEXT_PUBLIC_REPLY_TO_EMAIL=support@yatheeshnagella.com
Email Workflows
1. User Signup Flow
// app/api/auth/signup/route.ts
const { user } = await signUpWithInvite(email, password, name, inviteCode);
// Send welcome email (async, doesn't block response)
sendEmail({
to: email,
subject: 'Welcome to OneLibro',
templateKey: 'welcome_email',
templateProps: { name },
category: 'transactional',
}).catch(console.error);
2. Invite Code Delivery
// app/api/admin/invites/create/route.ts
const inviteCode = await createInviteCode(email, maxUses);
await sendEmail({
to: email,
subject: 'Your OneLibro Invite Code',
templateKey: 'invite_code_email',
templateProps: {
code: inviteCode,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
},
category: 'transactional',
});
3. Budget Alerts (Cron Job)
// app/api/cron/budget-alerts/route.ts
export async function GET(request: NextRequest) {
// Runs daily at 9 AM UTC
const overBudgetUsers = await findUsersOverBudget();
for (const user of overBudgetUsers) {
await sendEmail({
to: user.email,
subject: 'Budget Alert: You\'ve exceeded your budget',
templateKey: 'budget_alert_email',
templateProps: {
name: user.name,
budgetName: user.budget.name,
spent: user.totalSpent,
limit: user.budget.limit,
},
category: 'system',
});
// Log alert to prevent duplicates
await logBudgetAlert(user.budget.id, user.id);
}
}
Testing Emails
Development Testing
Use Resend's test mode to preview emails without sending:
// Set in .env.local
RESEND_API_KEY=re_test_xxxxxxxxxxxxxxxxxxxx
Preview Templates Locally
Run the email preview server:
cd emails
npm run dev
Visit http://localhost:3000 to preview all email templates with sample data.
Send Test Email (Admin Dashboard)
Admins can send test emails from the admin dashboard:
- Navigate to
/admin/emails/templates - Select a template
- Click "Send Test Email"
- Enter test recipient email
- Review delivery in Resend dashboard
Monitoring & Analytics
Resend Dashboard
Track email delivery metrics at https://resend.com/emails:
- Sent: Successfully delivered to recipient's server
- Delivered: Accepted by recipient's inbox
- Opened: Recipient opened the email (requires tracking pixel)
- Clicked: Recipient clicked a link
- Bounced: Delivery failed (invalid email, mailbox full)
- Complained: Recipient marked as spam
Database Queries
Check recent email logs:
-- Recent emails sent
SELECT email_to, template_key, subject, status, sent_at
FROM email_logs
ORDER BY sent_at DESC
LIMIT 50;
-- Failed emails
SELECT email_to, template_key, error_message, sent_at
FROM email_logs
WHERE status = 'failed'
ORDER BY sent_at DESC;
-- Emails by category
SELECT category, COUNT(*) as count,
COUNT(CASE WHEN status = 'sent' THEN 1 END) as successful
FROM email_logs
GROUP BY category;
Notification Preferences
Users can manage email preferences via /finance/settings/notifications.
notification_preferences Table
CREATE TABLE notification_preferences (
user_id UUID PRIMARY KEY REFERENCES users(id),
budget_alerts BOOLEAN DEFAULT true,
account_updates BOOLEAN DEFAULT true,
marketing_emails BOOLEAN DEFAULT false,
updated_at TIMESTAMPTZ DEFAULT now()
);
Respecting User Preferences
// Check preferences before sending
const { preferences } = await getUserNotificationPreferences(userId);
if (preferences.budget_alerts) {
await sendEmail({ /* budget alert */ });
}
Unsubscribe Links
All marketing emails include an unsubscribe link:
<Footer>
<a href={`https://finance.yatheeshnagella.com/api/notifications/unsubscribe?token=${token}`}>
Unsubscribe
</a>
</Footer>
Best Practices
1. Async Email Sending
Never block API responses waiting for email delivery:
// ✅ Good - Fire and forget
sendEmail(options).catch(console.error);
// ❌ Bad - Blocks response
await sendEmail(options);
2. Error Handling
Always handle email failures gracefully:
try {
await sendEmail(options);
} catch (error) {
console.error('Email failed:', error);
// Don't fail the entire operation
// Log to database for review
}
3. Rate Limiting
Resend limits vary by plan. For batch emails, implement rate limiting:
import pLimit from 'p-limit';
const limit = pLimit(10); // 10 concurrent emails
const promises = users.map(user =>
limit(() => sendEmail({ to: user.email, /* ... */ }))
);
await Promise.all(promises);
4. Template Versioning
When updating email templates, test thoroughly:
- Send test emails to yourself
- Check rendering across email clients (Gmail, Outlook, Apple Mail)
- Verify links work correctly
- Test on mobile devices
Troubleshooting
Email Not Sending
- Check Resend Dashboard: Look for errors or bounces
- Verify API Key: Ensure
RESEND_API_KEYis set correctly - Check Logs: Review
email_logstable for error messages - Domain Verification: Ensure sending domain is verified in Resend
Template Not Rendering
- Check Template Key: Ensure it exists in
email_templatestable - Verify Props: Check that all required props are passed
- Review React Errors: Check server logs for React rendering errors
Users Not Receiving Emails
- Spam Folder: Ask users to check spam/junk folders
- Email Validity: Verify email address is valid
- Domain Reputation: Check Resend domain reputation
- User Preferences: Verify user hasn't unsubscribed
Future Enhancements
- Email campaign builder for marketing
- A/B testing for subject lines
- Advanced segmentation for targeted emails
- Email scheduling (send at optimal times)
- Rich analytics dashboard
- SMS notifications via Twilio integration