Complete guide to TuvixRSS's transactional email system, powered by Resend and React Email.
- Overview
- Architecture
- Setup and Configuration
- Email Types
- Email Flows
- Template Development
- API Reference
- Troubleshooting
- Code References
TuvixRSS uses Resend for transactional email delivery, supporting email verification, password reset, and welcome emails. All email templates are built using React Email components, providing a modern, type-safe approach to email development.
- Email Verification: Verify user email addresses during registration
- Password Reset: Secure password reset via email tokens
- Welcome Emails: Greet new users after successful registration
- Graceful Fallback: Development mode logging when API key is missing
- Type-Safe Templates: React Email components with TypeScript
- Unified Sending Logic: Shared email sending infrastructure
Core Service: packages/api/src/services/email.ts
All email sending logic is centralized in this file, with templates in packages/api/src/services/email-templates/.
packages/api/src/services/
├── email.ts # Core email service (sending logic)
└── email-templates/
├── index.ts # Template exports
├── verification.tsx # Email verification template
├── password-reset.tsx # Password reset template
└── welcome.tsx # Welcome email template
- Email Function Called (e.g.,
sendVerificationEmail) - Configuration Check - Verifies
RESEND_API_KEYandEMAIL_FROMare set - Dev Mode Fallback - If not configured, logs to console (dev mode only)
- Resend Client - Initializes Resend client with API key
- Template Rendering - React Email component rendered to HTML
- Email Sent - Resend API sends email
- Result Logging - Success/failure logged (dev mode)
All email types use a shared sendEmail() helper function that handles:
- Configuration checking
- Resend client initialization
- Error handling
- Success/failure logging
- Development mode fallback
Non-Blocking Behavior:
- Email sending failures do not block user registration or other operations
- Failures are logged to console but don't throw errors
- User registration completes successfully even if email sending fails
- This ensures availability - users can always register, even if email service is down
Implementation: packages/api/src/services/email.ts:96
async function sendEmail(options: SendEmailOptions): Promise<SendEmailResult> {
// Common logic for all email types
}- Resend Account - Sign up at resend.com
- Domain Verification - Verify your sending domain in Resend dashboard
- API Key - Create an API key in Resend dashboard
- Sign up at resend.com
- Verify your email address
- Complete account setup
- Navigate to "Domains" in Resend dashboard
- Click "Add Domain"
- Enter your domain (e.g.,
yourdomain.com) - Add DNS records provided by Resend:
- DKIM - DomainKeys Identified Mail
- SPF - Sender Policy Framework
- DMARC - Domain-based Message Authentication
- Click "Verify DNS Records" in Resend dashboard
- Wait for verification (usually a few minutes, up to 24-48 hours)
Important: The EMAIL_FROM address must match a verified domain in Resend.
- Navigate to "API Keys" in Resend dashboard
- Click "Create API Key"
- Give it a descriptive name (e.g., "TuvixRSS Production")
- Copy the API key (starts with
re_) - Store securely (you won't be able to see it again)
Add to your .env file:
# Required for email functionality
RESEND_API_KEY=re_xxxxxxxxx
EMAIL_FROM=noreply@yourdomain.com # Must match verified domain
BASE_URL=http://localhost:5173 # Frontend URL for email linksSet secrets via Wrangler CLI:
cd packages/api
# Required secrets
npx wrangler secret put RESEND_API_KEY
# Enter: re_xxxxxxxxx
npx wrangler secret put EMAIL_FROM
# Enter: noreply@yourdomain.com
npx wrangler secret put BASE_URL
# Enter: https://yourdomain.com- Register a new user - Should receive verification email (if enabled) and welcome email
- Request password reset - Should receive password reset email
- Check Resend dashboard - View delivery status and logs
Purpose: Verify user email addresses during registration
Template: packages/api/src/services/email-templates/verification.tsx
Subject: "Verify Your TuvixRSS Email Address"
When Sent:
- Automatically on registration (if
requireEmailVerificationis enabled) - Manually via
auth.resendVerificationEmailendpoint
Parameters:
to: Recipient email addressusername: User's display nameverificationToken: Verification tokenverificationUrl: Full verification URL
Function: sendVerificationEmail(env, params)
Example:
import { sendVerificationEmail } from "@/services/email";
await sendVerificationEmail(env, {
to: user.email,
username: user.name || "User",
verificationToken: token,
verificationUrl: `${baseUrl}/api/auth/verify-email?token=${token}`,
});Purpose: Allow users to reset forgotten passwords
Template: packages/api/src/services/email-templates/password-reset.tsx
Subject: "Reset Your TuvixRSS Password"
When Sent:
- When user requests password reset via
auth.requestPasswordReset - Triggered by Better Auth's
requestPasswordResetendpoint
Parameters:
to: Recipient email addressusername: User's display nameresetToken: Password reset tokenresetUrl: Full password reset URL
Function: sendPasswordResetEmail(env, params)
Example:
import { sendPasswordResetEmail } from "@/services/email";
await sendPasswordResetEmail(env, {
to: user.email,
username: user.name || "User",
resetToken: token,
resetUrl: `${baseUrl}/reset-password?token=${token}`,
});Purpose: Greet new users after successful registration
Template: packages/api/src/services/email-templates/welcome.tsx
Subject: "Welcome to Tuvix!"
When Sent:
- Automatically after successful registration
- Only if email verification is not required OR user is already verified
Parameters:
to: Recipient email addressusername: User's display nameappUrl: Frontend application URL
Function: sendWelcomeEmail(env, params)
Example:
import { sendWelcomeEmail } from "@/services/email";
await sendWelcomeEmail(env, {
to: user.email,
username: user.name || "User",
appUrl: baseUrl,
});Email Verification Bypass:
- Admin users bypass email verification checks in tRPC middleware
- Admins can access protected endpoints even if
emailVerifiedisfalse - This allows first admin user (created via
ALLOW_FIRST_USER_ADMIN) immediate access
Email Sending Behavior:
- Verification emails are still sent to admins if
requireEmailVerificationis enabled - Welcome emails follow normal logic: sent if verification not required OR user is verified
- Note: If
requireEmailVerificationis enabled and admin hasn't verified email, welcome email is delayed until verification (even though admin can access app) - Admin status doesn't prevent email sending, only bypasses access restrictions
Implementation: packages/api/src/trpc/init.ts:68-78
- User Registers → Better Auth creates account
- Role Assignment:
- First user may be promoted to admin (if
ALLOW_FIRST_USER_ADMINis enabled) - Admin users bypass email verification requirement (can access app immediately)
- First user may be promoted to admin (if
- Email Verification Check:
- If
requireEmailVerificationis enabled:- Verification email sent automatically (even for admins)
- Welcome email not sent (waiting for verification)
- Exception: Admin users can access app without verification
- If
requireEmailVerificationis disabled:- Welcome email sent immediately
- If
- User Verifies Email (if required):
- Clicks verification link
- Email verified via Better Auth
- Welcome email sent (if not already sent)
Admin Bypass: Admin users bypass email verification checks in middleware (packages/api/src/trpc/init.ts:68), allowing immediate access even if emailVerified is false. However, verification emails are still sent if requireEmailVerification is enabled.
Implementation: packages/api/src/auth/better-auth.ts:320
- User Requests Reset →
auth.requestPasswordResetendpoint - Better Auth Generates Token → Secure 32-byte token
- Email Sent →
sendPasswordResetEmailcalled - User Clicks Link → Redirected to reset password page
- User Submits New Password →
auth.resetPasswordendpoint - Token Validated → Better Auth verifies token
- Password Updated → User can log in with new password
Implementation: packages/api/src/routers/auth.ts:552
- User Registers → Account created, email not verified
- Verification Email Sent →
sendVerificationEmailcalled (ifrequireEmailVerificationis enabled) - User Clicks Link → Better Auth
/api/auth/verify-emailendpoint - Token Validated → Better Auth verifies token
- Email Verified →
emailVerifiedflag set totrue - Welcome Email Sent → If not already sent
Resend Verification Email:
- User can request new verification email via
auth.resendVerificationEmail - Rate limited: 1 request per 5 minutes per user
- Only available if
requireEmailVerificationis enabled and user is not already verified
Admin Users:
- Admin users bypass email verification requirement in middleware
- Can access all protected endpoints without verifying email
- Verification emails are still sent if
requireEmailVerificationis enabled - Welcome emails are sent immediately if
requireEmailVerificationis disabled OR if admin is already verified
Implementation:
- Email sending:
packages/api/src/routers/auth.ts:355 - Admin bypass:
packages/api/src/trpc/init.ts:68
React Email provides a development server for previewing email templates in the browser.
Start Preview Server:
# From project root
pnpm run email:preview
# Or from packages/api directory
cd packages/api
pnpm run email:previewThis starts a local preview server (typically at http://localhost:3000) where you can:
- View all email templates in a browser
- See how templates render with different props
- Test responsive design
- Preview in different email clients (via React Email's built-in previews)
Preview Server Features:
- Hot reload - changes to templates update automatically
- Multiple template preview - see all templates side-by-side
- Props editor - modify template props in real-time
- Email client previews - see how emails look in Gmail, Outlook, etc.
Note: The preview server looks for templates in packages/api/src/services/email-templates/ directory.
Testing with Sample Data: When previewing templates, you can test with different prop values:
- Verification Email: Test with various usernames and verification URLs
- Password Reset: Test with different reset URLs and usernames
- Welcome Email: Test with different app URLs and usernames
The preview server allows you to modify props in real-time to see how templates render with different data.
All email templates follow this structure:
import {
Body,
Button,
Container,
Head,
Heading,
Html,
Preview,
Section,
Text,
} from "@react-email/components";
import * as React from "react";
interface MyEmailProps {
username: string;
actionUrl: string;
}
export const MyEmail: React.FC<Readonly<MyEmailProps>> = ({
username,
actionUrl,
}) => (
<Html>
<Head />
<Preview>Email preview text</Preview>
<Body style={main}>
<Container style={container}>
<Heading style={h1}>Email Title</Heading>
<Text style={text}>Hello {username},</Text>
{/* Email content */}
<Section style={buttonContainer}>
<Button style={button} href={actionUrl}>
Action Button
</Button>
</Section>
{/* Additional content */}
</Container>
</Body>
</Html>
);
export default MyEmail;
// Styles
const main = {
/* ... */
};
const container = {
/* ... */
};
const h1 = {
/* ... */
};
const text = {
/* ... */
};
const buttonContainer = {
/* ... */
};
const button = {
/* ... */
};-
Create Template (
packages/api/src/services/email-templates/my-email.tsx):export const MyEmail: React.FC<MyEmailProps> = ({ ... }) => ( // Template JSX );
-
Export Template (
packages/api/src/services/email-templates/index.ts):export { MyEmail } from "./my-email";
-
Add Type Definition (
packages/api/src/services/email.ts):export interface MyEmailParams { to: string; username: string; // ... other params }
-
Add Sending Function (
packages/api/src/services/email.ts):export async function sendMyEmail( env: Env, params: MyEmailParams ): Promise<SendEmailResult> { return sendEmail({ env, to: params.to, subject: "My Email Subject", template: MyEmail({ ...params }) as React.ReactElement, type: "my-email", details: { ...params }, }); }
-
Use Function:
import { sendMyEmail } from "@/services/email"; await sendMyEmail(env, { to: user.email, username: user.name, // ... other params });
- Preview Text: Always include a
<Preview>component - Responsive Design: Use inline styles (email clients don't support CSS)
- Accessibility: Use semantic HTML and alt text for images
- Fallback Links: Include plain text links if buttons don't work
- Expiration Notice: Mention token/link expiration times
- Brand Consistency: Match your application's design system
- Inline Styles Only: Email clients don't support
<style>tags - Table-Based Layouts: Use tables for complex layouts (better email client support)
- Web-Safe Fonts: Use system font stacks
- Color Contrast: Ensure sufficient contrast for accessibility
- Button Styling: Use background colors, not borders (better support)
Send email verification email.
function sendVerificationEmail(
env: Env,
params: VerificationEmailParams
): Promise<SendEmailResult>;Parameters:
env: Environment configurationparams.to: Recipient email addressparams.username: User's display nameparams.verificationToken: Verification tokenparams.verificationUrl: Full verification URL
Returns: Promise<SendEmailResult>
Send password reset email.
function sendPasswordResetEmail(
env: Env,
params: PasswordResetEmailParams
): Promise<SendEmailResult>;Parameters:
env: Environment configurationparams.to: Recipient email addressparams.username: User's display nameparams.resetToken: Password reset tokenparams.resetUrl: Full password reset URL
Returns: Promise<SendEmailResult>
Send welcome email to new user.
function sendWelcomeEmail(
env: Env,
params: WelcomeEmailParams
): Promise<SendEmailResult>;Parameters:
env: Environment configurationparams.to: Recipient email addressparams.username: User's display nameparams.appUrl: Frontend application URL
Returns: Promise<SendEmailResult>
interface SendEmailResult {
success: boolean;
error?: string;
}interface VerificationEmailParams {
to: string;
username: string;
verificationToken: string;
verificationUrl: string;
}interface PasswordResetEmailParams {
to: string;
username: string;
resetToken: string;
resetUrl: string;
}interface WelcomeEmailParams {
to: string;
username: string;
appUrl: string;
}Symptoms: No emails received, no errors in logs
Solutions:
- Verify API Key: Check
RESEND_API_KEYis set correctly - Verify Domain: Ensure
EMAIL_FROMmatches verified domain in Resend - Check Resend Dashboard: View API errors and delivery status
- Development Mode: Check console logs (emails log to console if API key missing)
- Check Spam Folder: Emails may be filtered as spam
Symptoms: Emails fail with domain verification errors
Solutions:
- Verify DNS Records: Ensure all DNS records (DKIM, SPF, DMARC) are added correctly
- Wait for Propagation: DNS changes can take 24-48 hours
- Use Resend Tool: Use Resend's DNS verification tool to check record status
- Check Record Format: Ensure records match exactly what Resend provides
Symptoms: Emails arrive but with significant delay
Solutions:
- Check Resend Dashboard: View delivery status and logs
- Verify Recipient Email: Ensure email address is valid
- Check Spam Filters: Review spam/junk folders
- Review Resend Logs: Check for bounce/spam reports
When RESEND_API_KEY is not configured, emails are logged to console instead of being sent:
📧 Email (verification) would be sent to: user@example.com
Details: {
"username": "John",
"verificationUrl": "http://localhost:5173/api/auth/verify-email?token=..."
}
⚠️ Configure RESEND_API_KEY and EMAIL_FROM to send emails
This allows development without a Resend account, but emails won't actually be sent.
Symptoms: Emails render incorrectly or buttons don't work
Solutions:
- Test in Email Clients: Use tools like Litmus or Email on Acid
- Check Inline Styles: Ensure all styles are inline (not in
<style>tags) - Verify Button Links: Test that
hrefattributes are correct - Check HTML Structure: Ensure proper nesting and closing tags
| File | Description |
|---|---|
packages/api/src/services/email.ts |
Core email service and sending functions |
packages/api/src/services/email-templates/verification.tsx |
Email verification template |
packages/api/src/services/email-templates/password-reset.tsx |
Password reset template |
packages/api/src/services/email-templates/welcome.tsx |
Welcome email template |
packages/api/src/services/email-templates/index.ts |
Template exports |
packages/api/src/auth/better-auth.ts |
Better Auth email verification integration |
packages/api/src/routers/auth.ts |
Auth router with email endpoints |
Better Auth Email Verification:
packages/api/src/auth/better-auth.ts:178-emailVerification.sendVerificationEmailcallbackpackages/api/src/auth/better-auth.ts:345- Manual verification email on sign-up
Password Reset:
packages/api/src/auth/better-auth.ts:92-sendResetPasswordcallbackpackages/api/src/routers/auth.ts:552-requestPasswordResetendpoint
Welcome Email:
packages/api/src/auth/better-auth.ts:358- Welcome email after sign-up
Resend Verification Email:
packages/api/src/routers/auth.ts:355-resendVerificationEmailendpoint
- Authentication Guide - Email verification integration
- Security Guide - Email service security considerations
- Admin Guide - Email configuration for admins
- Deployment Guide - Environment variable configuration
Last Updated: 2025-01-15