Email Configuration
Set up and customize transactional emails with Resend and React Email
Transactional emails are already configured and working. Unstack Pro uses Resend for reliable email delivery and React Email for beautiful, customizable email templates.
Email verification is required for users to sign in. You must configure Resend for the application to work.
Overview
Unstack Pro includes a complete email system:
- Resend for reliable email delivery
- React Email for writing email templates in React
- Pre-built templates for all auth flows
- Easy customization with familiar React patterns
Setting Up Resend
Create a Resend Account
- Go to resend.com
- Sign up for a free account
- Verify your email address
Get Your API Key
- Go to your Resend dashboard
- Navigate to API Keys
- Click Create API Key
- Give it a name (e.g., "Unstack Pro Production")
- Copy the key (starts with
re_)
Configure Environment Variables
RESEND_API_KEY and EMAIL_FROM belong wherever the code that sends the mail
runs:
Sending happens in ordinary Next.js server code, so both go in .env.local:
RESEND_API_KEY="re_your_api_key_here"
EMAIL_FROM="Your App <no-reply@yourdomain.com>"Sending happens on the Convex backend, so both belong on the Convex deployment:
bunx convex env set RESEND_API_KEY "re_your_api_key_here"
bunx convex env set EMAIL_FROM "Your App <no-reply@yourdomain.com>"Verify Your Domain (Production)
For production, you need to verify your sending domain:
- In Resend dashboard, go to Domains
- Click Add Domain
- Enter your domain (e.g.,
yourdomain.com) - Add the DNS records Resend provides
- Wait for verification (usually a few minutes)
Without domain verification, emails may go to spam or fail to send. During development, you can use Resend's test mode which only sends to verified email addresses.
Email Templates
All email templates are located in the /emails directory and built with React Email:
emails/
├── verification-email.tsx # Email verification OTP
├── reset-password-email.tsx # Password reset OTP
├── signin-otp-email.tsx # OTP sign-in code
├── reauth-otp-email.tsx # Re-authentication OTP
├── password-changed-email.tsx # Password change notice
├── organization-invitation-email.tsx # Organization invitations
├── delete-account-verification.tsx # Account deletion confirmation
└── index.ts # Barrel the send layer imports fromAvailable Templates
| Template | Purpose | When Sent |
|---|---|---|
verification-email | Verify new user's email | After registration, and when a user changes their email (a 6-digit code is sent to the new address) |
reset-password-email | Password reset code | When user requests password reset |
signin-otp-email | OTP sign-in code | When user chooses OTP login |
reauth-otp-email | Re-authentication code | When a sensitive action asks the user to prove who they are |
password-changed-email | Password change notice | After a password is changed or reset |
organization-invitation-email | Invite to organization | When member is invited |
delete-account-verification | Confirm account deletion | When user initiates deletion |
Customizing Email Templates
React Email templates work like regular React components. Here's how to customize them:
Basic Template Structure
import {
Body,
Container,
Head,
Heading,
Html,
Preview,
Section,
Text,
} from "@react-email/components";
interface VerificationEmailProps {
otp: string;
appName: string;
}
export default function VerificationEmail({
otp,
appName,
}: VerificationEmailProps) {
return (
<Html>
<Head />
<Preview>Your verification code for {appName}</Preview>
<Body style={main}>
<Container style={container}>
<Heading style={h1}>Verify your email</Heading>
<Text style={text}>
Your verification code is:
</Text>
<Section style={codeContainer}>
<Text style={code}>{otp}</Text>
</Section>
<Text style={text}>
This code expires in 10 minutes.
</Text>
</Container>
</Body>
</Html>
);
}
// Styles
const main = {
backgroundColor: "#f6f9fc",
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
};
const container = {
backgroundColor: "#ffffff",
margin: "0 auto",
padding: "40px 20px",
borderRadius: "8px",
};
const h1 = {
color: "#1a1a1a",
fontSize: "24px",
fontWeight: "600",
margin: "0 0 20px",
};
const text = {
color: "#4a5568",
fontSize: "16px",
lineHeight: "24px",
margin: "0 0 16px",
};
const codeContainer = {
background: "#f4f4f5",
borderRadius: "8px",
padding: "16px",
textAlign: "center" as const,
};
const code = {
color: "#1a1a1a",
fontSize: "32px",
fontWeight: "700",
letterSpacing: "4px",
margin: "0",
};Customization Tips
Change Branding:
- Update colors to match your brand
- Add your logo using the
Imgcomponent - Modify copy to match your voice
Add Logo:
import { Img } from "@react-email/components";
<Img
src="https://yourdomain.com/logo.png"
alt="Your App"
width={120}
height={40}
/>Add Footer:
<Section style={footer}>
<Text style={footerText}>
© 2024 Your Company. All rights reserved.
</Text>
<Text style={footerText}>
123 Main Street, City, Country
</Text>
</Section>Email Sending Logic
One sendEmail helper renders a React Email template to HTML and hands it to
Resend, and the Better Auth config calls it from every auth flow. Where that
helper runs is the one real architectural difference between the editions.
lib/email.ts — ordinary Next.js server code, called straight from the Better
Auth config in lib/auth.ts.
import { render } from "@react-email/components";
import { Resend } from "resend";
export async function sendEmail<T extends EmailTemplate>(params: {
from: string;
to: string;
subject: string;
template: T;
props: TemplateProps[T];
}) {
const html = await render(TEMPLATES[params.template](params.props));
const resend = new Resend(process.env.RESEND_API_KEY!);
const response = await resend.emails.send({
from: params.from,
to: params.to,
subject: params.subject,
html,
});
if (response.error) {
throw new Error(`Failed to send email: ${response.error.message}`);
}
}convex/email.ts — a "use node" internal action on the Convex backend, which
the Better Auth config in convex/betterAuth/auth.ts schedules. Template props
cross the boundary as a JSON string, because Convex validators cannot describe a
React component's props.
"use node";
import { render } from "@react-email/components";
import { v } from "convex/values";
import { Resend } from "resend";
import { internalAction } from "./_generated/server";
export const sendEmail = internalAction({
args: {
from: v.string(),
to: v.string(),
subject: v.string(),
template: v.union(/* one literal per template */),
props: v.string(),
},
handler: async (_, args) => {
const html = await render(TEMPLATES[args.template](JSON.parse(args.props)));
const resend = new Resend(process.env.RESEND_API_KEY!);
await resend.emails.send({
from: args.from,
to: args.to,
subject: args.subject,
html,
});
},
});Email Deliverability
During development, emails may end up in spam. This is expected behavior before you verify your domain. See Resend's guide on email deliverability for more details.
Rate Limits
Resend has rate limits based on your plan:
| Plan | Emails/day |
|---|---|
| Free | 100 |
| Pro | 50,000+ |
For most SaaS applications, the free tier is sufficient during development. Upgrade as you scale.
Adding New Email Templates
To create a new email template:
- Create the template file:
import {
Body,
Container,
Head,
Heading,
Html,
Preview,
Text,
} from "@react-email/components";
interface WelcomeEmailProps {
name: string;
appName: string;
}
export default function WelcomeEmail({ name, appName }: WelcomeEmailProps) {
return (
<Html>
<Head />
<Preview>Welcome to {appName}!</Preview>
<Body style={main}>
<Container style={container}>
<Heading style={h1}>Welcome, {name}!</Heading>
<Text style={text}>
Thanks for joining {appName}. We're excited to have you.
</Text>
</Container>
</Body>
</Html>
);
}
// ... styles- Register it — export it from
emails/index.tsand add it to the template map in the send layer (see Email Sending Logic) - Call
sendEmailwith the new template name from your application logic
Environment Variables Summary
| Variable | Required | Where | Description |
|---|---|---|---|
RESEND_API_KEY | Yes | .env.local | Your Resend API key |
EMAIL_FROM | Yes | .env.local | Sender address (e.g., App <no-reply@domain.com>) |
| Variable | Required | Where | Description |
|---|---|---|---|
RESEND_API_KEY | Yes | Convex deployment | Your Resend API key |
EMAIL_FROM | Yes | Convex deployment | Sender address (e.g., App <no-reply@domain.com>) |