Emails
Emails transaccionales con Resend y React Email
Emails
El boilerplate usa Resend + React Email para emails transaccionales. Templates en JSX, alta deliverability y 3,000 emails/mes gratis.
Setup de Resend
- Crea una cuenta en resend.com
- Ve a API Keys > Create API Key y copia la key
- Agrega la variable de entorno:
RESEND_API_KEY="re_..."- (Recomendado) Verifica tu dominio en Domains del dashboard. Configura los registros DNS (MX, SPF, DKIM) y espera verificación.
Sin dominio verificado, solo puedes enviar a tu email registrado usando onboarding@resend.dev.
Las dependencias ya vienen incluidas en el boilerplate:
npm install resend @react-email/componentsConfiguración
Cliente Resend con lazy initialization
El boilerplate usa un patron lazy Proxy para evitar errores de build cuando RESEND_API_KEY no está configurada (CI, primer deploy):
import { Resend } from "resend";
import { siteConfig } from "@/config/site";
let _resend: Resend | null = null;
function getResend(): Resend {
if (!process.env.RESEND_API_KEY) {
throw new Error("RESEND_API_KEY is not set");
}
if (!_resend) {
_resend = new Resend(process.env.RESEND_API_KEY);
}
return _resend;
}
export const resend = new Proxy({} as Resend, {
get(_target, prop) {
return getResend()[prop as keyof Resend];
},
});
const FROM_EMAIL =
`${siteConfig.name} <${siteConfig.noReplyEmail}>`;El cliente no se instancia hasta que se use por primera vez. Esto permite que next build funcione sin RESEND_API_KEY en el entorno.
Remitente y email de soporte
El remitente se construye desde siteConfig. Cambia estos valores por los de tu dominio verificado:
export const siteConfig = {
name: "EmpiezaTuSaaS",
noReplyEmail: "noreply@empiezatusaas.com",
supportEmail: "soporte@empiezatusaas.com",
// ...
} as const;Estructura del directorio
emails/
├── welcome.tsx # Email de bienvenida
├── verify-email.tsx # Verificación de email
└── reset-password.tsx # Restablecimiento de contraseñaTemplates incluidos
Todos los templates usan componentes de @react-email/components y leen la configuración desde siteConfig.
| Template | Archivo | Componente | Disparado por |
|---|---|---|---|
| Verificación | emails/verify-email.tsx | VerifyEmailTemplate | Better Auth (registro) |
| Contraseña | emails/reset-password.tsx | ResetPasswordTemplate | Better Auth (olvido de contraseña) |
| Bienvenida | emails/welcome.tsx | WelcomeTemplate | Manualmente despues del registro |
VerifyEmailTemplate
Verifica la direccion de email del usuario. Se envia automáticamente por Better Auth.
import {
Body, Button, Container, Head, Heading,
Hr, Html, Preview, Text,
} from "@react-email/components";
import { siteConfig } from "@/config/site";
interface VerifyEmailTemplateProps {
name: string;
verificationUrl: string;
}
export function VerifyEmailTemplate({
name, verificationUrl,
}: VerifyEmailTemplateProps) {
return (
<Html>
<Head />
<Preview>Verifica tu email para acceder a {siteConfig.name}</Preview>
<Body style={main}>
<Container style={container}>
<Heading style={h1}>Verifica tu email</Heading>
<Text style={text}>Hola {name},</Text>
<Text style={text}>
Gracias por registrarte en {siteConfig.name}. Por favor, verifica tu
direccion de email haciendo clic en el boton de abajo.
</Text>
<Button style={button} href={verificationUrl}>
Verificar Email
</Button>
<Text style={text}>
Este enlace expira en 24 horas. Si no creaste está cuenta, puedes
ignorar este email.
</Text>
<Hr style={hr} />
<Text style={footer}>
{siteConfig.name} — {siteConfig.domain}
</Text>
</Container>
</Body>
</Html>
);
}ResetPasswordTemplate
Se envia cuando el usuario solicita restablecer su contraseña.
import {
Body, Button, Container, Head, Heading,
Hr, Html, Preview, Text,
} from "@react-email/components";
import { siteConfig } from "@/config/site";
interface ResetPasswordTemplateProps {
name: string;
resetUrl: string;
}
export function ResetPasswordTemplate({
name, resetUrl,
}: ResetPasswordTemplateProps) {
return (
<Html>
<Head />
<Preview>Restablece tu contraseña de {siteConfig.name}</Preview>
<Body style={main}>
<Container style={container}>
<Heading style={h1}>Restablece tu contraseña</Heading>
<Text style={text}>Hola {name},</Text>
<Text style={text}>
Recibimos una solicitud para restablecer la contraseña de tu cuenta
en {siteConfig.name}. Haz clic en el boton de abajo para crear una
nueva contraseña.
</Text>
<Button style={button} href={resetUrl}>
Restablecer Contraseña
</Button>
<Text style={text}>
Este enlace expira en 1 hora. Si no solicitaste este cambio, puedes
ignorar este email de forma segura.
</Text>
<Hr style={hr} />
<Text style={footer}>
{siteConfig.name} — {siteConfig.domain}
</Text>
</Container>
</Body>
</Html>
);
}WelcomeTemplate
Se envia al registrarse el usuario. Muestra un saludo personalizado y un boton al dashboard.
import {
Body, Button, Container, Head, Heading,
Hr, Html, Preview, Section, Text,
} from "@react-email/components";
import { siteConfig } from "@/config/site";
interface WelcomeTemplateProps {
name: string;
}
export function WelcomeTemplate({ name }: WelcomeTemplateProps) {
return (
<Html>
<Head />
<Preview>Bienvenido a {siteConfig.name}! Tu SaaS comienza aqui.</Preview>
<Body style={main}>
<Container style={container}>
<Heading style={h1}>Bienvenido, {name}!</Heading>
<Text style={text}>
Gracias por unirte a {siteConfig.name}. Estas a un paso de lanzar
tu SaaS mas rapido que nunca.
</Text>
<Section style={section}>
<Text style={text}>Con tu acceso puedes:</Text>
<Text style={listItem}>Clonar el boilerplate y empezar en minutos</Text>
<Text style={listItem}>Usar auth, pagos y emails ya configurados</Text>
<Text style={listItem}>Desplegar en Vercel con un clic</Text>
</Section>
<Button style={button} href={siteConfig.url + "/dashboard"}>
Ir al Dashboard
</Button>
<Hr style={hr} />
<Text style={footer}>
Tienes dudas? Respondenos a {siteConfig.supportEmail}
</Text>
</Container>
</Body>
</Html>
);
}Estilos compartidos
Los templates comparten estilos inline (los clientes de email no soportan <style> de forma consistente):
const main = {
backgroundColor: "#f6f9fc",
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
};
const container = {
backgroundColor: "#ffffff",
margin: "0 auto",
padding: "20px 0 48px",
maxWidth: "580px",
};
const button = {
backgroundColor: "#18181b",
borderRadius: "8px",
color: "#fff",
fontSize: "16px",
fontWeight: "600",
textDecoration: "none",
textAlign: "center" as const,
display: "block",
padding: "12px 24px",
margin: "24px 48px",
};Enviar emails
Todas las funciones siguen el mismo patron: reciben un objeto con los parametros, importan el template dinámicamente y envian via Resend.
sendVerificationEmail
export async function sendVerificationEmail({
to, name, verificationUrl,
}: { to: string; name: string; verificationUrl: string }) {
const { VerifyEmailTemplate } = await import("@/emails/verify-email");
const { createElement } = await import("react");
return getResend().emails.send({
from: FROM_EMAIL,
to,
subject: `Verifica tu email - ${siteConfig.name}`,
react: createElement(VerifyEmailTemplate, { name, verificationUrl }),
});
}sendPasswordResetEmail
export async function sendPasswordResetEmail({
to, name, resetUrl,
}: { to: string; name: string; resetUrl: string }) {
const { ResetPasswordTemplate } = await import("@/emails/reset-password");
const { createElement } = await import("react");
return getResend().emails.send({
from: FROM_EMAIL,
to,
subject: `Restablece tu contraseña - ${siteConfig.name}`,
react: createElement(ResetPasswordTemplate, { name, resetUrl }),
});
}sendWelcomeEmail
export async function sendWelcomeEmail({
to, name,
}: { to: string; name: string }) {
const { WelcomeTemplate } = await import("@/emails/welcome");
const { createElement } = await import("react");
return getResend().emails.send({
from: FROM_EMAIL,
to,
subject: `Bienvenido a ${siteConfig.name}!`,
react: createElement(WelcomeTemplate, { name }),
});
}Integracion con Better Auth
Las funciones se conectan con Better Auth en lib/auth.ts:
import { betterAuth } from "better-auth";
import { sendVerificationEmail, sendPasswordResetEmail } from "@/lib/email";
export const auth = betterAuth({
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
sendResetPassword: async ({ user, url }) => {
await sendPasswordResetEmail({
to: user.email,
name: user.name,
resetUrl: url,
});
},
},
emailVerification: {
sendVerificationEmail: async ({ user, url }) => {
await sendVerificationEmail({
to: user.email,
name: user.name,
verificationUrl: url,
});
},
sendOnSignUp: true,
},
});Agregar un nuevo email
- Crea el template en
emails/tu-template.tsxusando@react-email/components - Crea la funcion de envio en
lib/email.ts:
export async function sendCustomEmail({
to, /* tus props */
}: { to: string }) {
const { TuTemplate } = await import("@/emails/tu-template");
const { createElement } = await import("react");
return getResend().emails.send({
from: FROM_EMAIL,
to,
subject: `Tu asunto - ${siteConfig.name}`,
react: createElement(TuTemplate, { /* props */ }),
});
}- Invocala desde tu API route o server action.
Preview en desarrollo
Para previsualizar emails durante el desarrollo:
npx email dev --dir emailsAbre http://localhost:3000 para ver tus templates renderizados.
Recibir emails
El boilerplate configura supportEmail en siteConfig como direccion de contacto. Los templates de email incluyen este valor para que los usuarios puedan responder con dudas:
supportEmail: "soporte@empiezatusaas.com",Los templates usan siteConfig.supportEmail en el footer para que los usuarios sepan donde escribir. Si necesitas recibir emails programaticamente, Resend no soporta inbound emails - usa un servicio como Mailgun o configura un formulario de contacto.
Resend tiene un limite de 3,000 emails/mes en el plan gratuito. Para mas volumen, consulta sus planes de pago.