Send transactional email with Resend and React Email templates.
ShipNext uses Resend for transactional email and React Email for templates.
Email is split into two layers:
src/integrations/email/- third-party provider adapters, currently Resend.src/modules/email/- business templates, rendering, and send entrypoints.
Auth-related verification emails, reset password emails, and magic links are triggered from src/modules/auth/config.ts through sendEmail. You can also call the same API from any server-side logic.
Environment variables
Configure Resend in .env.local:
| Variable | Description |
|---|---|
RESEND_API_KEY | Resend API key, usually re_xxx |
RESEND_API_KEY=re_xxxxxxxxThe sender address is configured in config/website.ts as email.fromEmail, not read from MAIL_FROM_EMAIL in the current code. The address must match a verified Resend domain.
Before enabling email verification, magic links, or reset password, configure RESEND_API_KEY and verify your sending domain. For first local setup, you can temporarily disable auth.enableEmailVerification.
Resend setup
Create a Resend account and API key.
Add and verify your sending domain in the Resend dashboard.
Set RESEND_API_KEY in .env.local.
Set the sender in config/website.ts:
const emailConfig = {
provider: "resend",
fromEmail: "[email protected]",
};websiteConfig.metadata.title appears in the email header brand area.
Built-in templates
Templates live in src/modules/email/components/templates/ and are registered in get-template.ts.
| Template | Purpose | Default trigger |
|---|---|---|
verifyEmail | Email verification | Better Auth verification email |
resetPassword | Reset password link | Forgot password flow |
passwordChanged | Password changed confirmation | After successful reset |
magicLink | Passwordless sign-in link | Better Auth magic link plugin |
forgotPassword | Alternate forgot password copy | Available but reset flow uses resetPassword |
registration | Welcome email | Ready, but must be called from a business hook |
Template context types live in src/modules/email/types.ts:
verifyEmail: { url: string; name: string };
resetPassword: { url: string; name: string; expiresIn: string };
magicLink: { url: string; name: string; expiresIn: string };
passwordChanged: { name: string; supportUrl: string };
registration: { dashboardUrl: string; name: string };Example:
import { sendEmail } from "@/modules/email";
await sendEmail({
to: user.email,
template: "verifyEmail",
context: {
url,
name: user.name || user.email,
},
locale: "en",
});Preview templates
Run React Email locally:
npx react-email devThen open the preview route provided by the project.
Send template email
Call sendEmail from server-side code:
import { sendEmail } from "@/modules/email";
const success = await sendEmail({
to: "[email protected]",
template: "registration",
context: {
name: "Alice",
dashboardUrl: "https://yourdomain.com/dashboard",
},
locale: "en",
});sendEmail returns a boolean. Use the provider directly if you need provider-specific response details.
Send raw HTML email
await sendEmail({
to: "[email protected]",
subject: "Custom subject",
html: "<p>Hello</p>",
text: "Hello",
});Auth dependencies
These features require email to be configured:
| Config | Email used |
|---|---|
auth.enableEmailVerification | verifyEmail |
| Forgot password | resetPassword |
auth.enableMagicLinkLogin | magicLink |
Welcome email
The registration template exists but is not automatically sent. Add it to databaseHooks.user.create.after if needed:
databaseHooks: {
user: {
create: {
after: async (user, request) => {
const locale = getLocaleFromRequest(request);
await sendEmail({
to: user.email,
template: "registration",
context: {
name: user.name || user.email,
dashboardUrl: `${websiteConfig.metadata.url}/dashboard`,
},
locale,
});
},
},
},
},Internationalization
Email copy lives in:
src/i18n/messages/en/email.json
src/i18n/messages/zh/email.jsonEach template needs a subject and the copy keys used by its React Email component. When adding a language, copy and translate email.json, then pass that locale to sendEmail.
Custom template
Create a React Email component
Add a file in src/modules/email/components/templates/ and reuse shared components such as EmailLayout, EmailButton, and EmailHeading.
Register the template
Add it to EmailTemplates in get-template.ts, add the name to emailTemplates, and declare its context type in types.ts.
Add translations
Update every email.json with the new template copy.
Send it
await sendEmail({
to: "[email protected]",
template: "myEmail",
context: { name: "Alice" },
locale: "en",
});Transactional email vs newsletter
| Capability | Config | Responsibility |
|---|---|---|
| Transactional email | websiteConfig.email | Verification, reset password, magic links |
| Newsletter | websiteConfig.newsletter | Marketing subscription status |
Both may use RESEND_API_KEY, but their APIs and modules are separate.
Troubleshooting
RESEND_API_KEY environment variable is not set
Any path that instantiates ResendProvider requires the key. Configure it before enabling email verification or magic links.
Email sends but never arrives
- Verify the domain in Resend.
- Confirm
fromEmailuses that verified domain. - Check Resend logs and spam folders.
Wrong email language
Pass locale explicitly or make sure the triggering request has a URL or headers that can be used to infer the desired locale.