This guide shows how to set up Resend + React Email inside a Better T Stack monorepo. The goal is to keep email logic isolated in a shared package while making it easy to use from your applications (web, server, authentication flows like password reset).
This setup works especially well with Better Auth, but it’s not tied to it — any backend or server action can use the same email package.
Project Structure Overview
The monorepo follows a typical Better T Stack layout:
apps/
├─ web
└─ server
packages/
├─ config
└─ transactional
- apps/ Application code (frontend and backend)
- packages/ Shared libraries
- packages/transactional/ Transactional email package (React Email + Resend)
- packages/config/ Shared TypeScript configuration
This structure keeps email templates reusable, typed, and independent from application logic.
Shared Configuration Package (@project/config)
Before looking at emails, it’s important to understand the shared config package. This is what makes JSX, strict typing, and React Email work consistently across the monorepo.
Instead of duplicating tsconfig settings in every app and package, everything extends a single base configuration.
Why This Matters
Email templates:
- Use JSX
- Live in shared packages (not Next.js apps)
- Are compiled in multiple environments
Without a shared config, you quickly run into:
- JSX not being enabled in packages
- Different module resolutions
- Subtle build failures between apps
packages/config/package.json
{
"name": "@project/config",
"version": "0.0.0",
"private": true
}
This package is:
- Private (not published)
- Used only inside the monorepo
- Referenced via workspace imports
packages/config/tsconfig.base.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ESNext"],
"verbatimModuleSyntax": true,
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"types": ["bun"],
"jsx": "react-jsx"
}
}
Important:
The"jsx": "react-jsx"option is what allows React Email templates (.tsx) to compile correctly inside shared packages.
Using the Shared Config in Packages
Each package extends the base config.
Example for the transactional email package:
// packages/transactional/tsconfig.json
{
"extends": "@project/config/tsconfig.base.json",
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"composite": true
}}
This ensures:
- JSX works everywhere
- Strict typing is consistent
- Bun, ESNext, and React Email all align
Transactional Email Package Setup
The transactional email logic lives in packages/transactional.
packages/transactional/package.json
{
"name": "@project/transactional",
"type": "module",
"exports": {
".": {
"default": "./src/index.tsx"
},
"./*": {
"default": "./src/*.tsx"
}
},
"devDependencies": {
"@remedy/config": "workspace:*",
"@types/react": "19.2.7",
"typescript": "catalog:"
},
"dependencies": {
"@react-email/components": "1.0.2",
"react-email": "5.1.0",
"resend": "6.6.0"
}
}
This package:
- Exposes email templates
- Sends emails via Resend
- Can be imported by any app in the monorepo
Creating Email Templates
Email templates are React components stored under:
packages/transactional/src/emails/
Example: Password Reset Email - used react email notion template
import {
Body,
Container,
Head,
Heading,
Html,
Link,
Preview,
Tailwind,
Text,
} from "@react-email/components";
interface DefaultEmailProps {
url?: string;
}
export const DefaultEmail = ({ url }: DefaultEmailProps) => (
<Html>
<Head />
<Tailwind>
<Body className="bg-white font-notion">
<Preview>Reset your password</Preview>
<Container className="px-3 mx-auto">
<Heading className="text-[#333] text-[24px] my-10">
Reset your password
</Heading>
<Link
href={url}
target="_blank"
className="text-[#2754C5] text-[14px] underline mb-4 block"
>
Click here to reset your password
</Link>
<Text className="text-[#333] text-[14px] my-6">
Or copy and paste this link into your browser:
</Text>
<code className="block p-4 bg-[#f4f4f4] rounded border text-[#333]">
{url}
</code>
<Text className="text-[#ababab] text-[14px] mt-4">
If you didn’t request a password reset, you can safely ignore this email.
</Text>
</Container>
</Body>
</Tailwind>
</Html>
);
Because this is just React:
- Templates are reusable
- Props are type-safe
- Styling stays consistent
Sending Emails with Resend
The sending logic is centralized in the package entry point.
packages/transactional/src/index.tsx
import { Resend } from "resend";
import { DefaultEmail } from "./emails/default";
const resend = new Resend(process.env.RESEND_API_KEY);
export const sendEmail = async ({
to,
subject,
url,
}: {
to: string;
subject: string;
url: string;
}) => {
return resend.emails.send({
from: process.env.EMAIL_FROM ?? "Acme <onboarding@resend.dev>",
to,
subject,
react: <DefaultEmail url={url} />,
});
};
This keeps:
- API keys out of app code
- Email logic centralized
- Templates decoupled from business logic
Using the Email Package in an App
1. Install the package
bun add @project/transactional
2. Configure environment variables
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxxx
EMAIL_FROM="Your App <noreply@yourdomain.com>"
3. Send an email
import { sendEmail } from "@remedy/transactional";
await sendEmail({
to: "user@example.com",
subject: "Reset your password",
url: "https://yourapp.com/reset-password?token=abc123",
});
This fits cleanly into:
- Password reset flows
- Email verification
- Invitations
- System notifications
Adding More Email Templates
To add another email (for example, a welcome email):
- Create a new file in
emails/ - Export a React component
- Import and use it in your sender logic
You can either:
- Add new helper functions per template, or
- Build a single
sendTransactionalEmailwrapper
Local Development & Preview
React Email provides a preview server:
cd packages/transactional
bunx email dev
Open:
http://localhost:3000
This lets you iterate on templates without sending real emails.
Best Practices
- Keep email logic out of app code
- Use strict typing for template props
- Reuse components (headers, footers, buttons)
- Preview emails locally
- Test sending in staging before production
- Wrap sending logic with error handling
Deployment Notes
Before deploying:
- Verify
RESEND_API_KEYandEMAIL_FROM - Confirm sender/domain in Resend
- Test email flows in staging
Final Thoughts
This setup gives you:
- Clean separation of concerns
- Typed, reusable email templates
- Consistent builds across the monorepo
- A strong foundation for auth-related emails (including Better Auth)
The shared @project/config package is the key that makes everything work smoothly — especially JSX inside shared packages.
If you want next steps, we can:
- Wire this directly into Better Auth password reset
- Introduce shared ESLint/Biome config alongside
@project/config
This is production-ready foundation.
Top comments (0)