Unstack Pro Docs

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

  1. Go to resend.com
  2. Sign up for a free account
  3. Verify your email address

Get Your API Key

  1. Go to your Resend dashboard
  2. Navigate to API Keys
  3. Click Create API Key
  4. Give it a name (e.g., "Unstack Pro Production")
  5. 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:

.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:

  1. In Resend dashboard, go to Domains
  2. Click Add Domain
  3. Enter your domain (e.g., yourdomain.com)
  4. Add the DNS records Resend provides
  5. 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 from

Available Templates

TemplatePurposeWhen Sent
verification-emailVerify new user's emailAfter registration, and when a user changes their email (a 6-digit code is sent to the new address)
reset-password-emailPassword reset codeWhen user requests password reset
signin-otp-emailOTP sign-in codeWhen user chooses OTP login
reauth-otp-emailRe-authentication codeWhen a sensitive action asks the user to prove who they are
password-changed-emailPassword change noticeAfter a password is changed or reset
organization-invitation-emailInvite to organizationWhen member is invited
delete-account-verificationConfirm account deletionWhen user initiates deletion

Customizing Email Templates

React Email templates work like regular React components. Here's how to customize them:

Basic Template Structure

emails/verification-email.tsx
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 Img component
  • 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.

lib/email.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.

convex/email.ts
"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:

PlanEmails/day
Free100
Pro50,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:

  1. Create the template file:
emails/welcome-email.tsx
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
  1. Register it — export it from emails/index.ts and add it to the template map in the send layer (see Email Sending Logic)
  2. Call sendEmail with the new template name from your application logic

Environment Variables Summary

VariableRequiredWhereDescription
RESEND_API_KEYYes.env.localYour Resend API key
EMAIL_FROMYes.env.localSender address (e.g., App <no-reply@domain.com>)
VariableRequiredWhereDescription
RESEND_API_KEYYesConvex deploymentYour Resend API key
EMAIL_FROMYesConvex deploymentSender address (e.g., App <no-reply@domain.com>)

Resources

Next Steps

On this page