ShipNext

Email

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:

VariableDescription
RESEND_API_KEYResend API key, usually re_xxx
RESEND_API_KEY=re_xxxxxxxx

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

TemplatePurposeDefault trigger
verifyEmailEmail verificationBetter Auth verification email
resetPasswordReset password linkForgot password flow
passwordChangedPassword changed confirmationAfter successful reset
magicLinkPasswordless sign-in linkBetter Auth magic link plugin
forgotPasswordAlternate forgot password copyAvailable but reset flow uses resetPassword
registrationWelcome emailReady, 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 dev

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

ConfigEmail used
auth.enableEmailVerificationverifyEmail
Forgot passwordresetPassword
auth.enableMagicLinkLoginmagicLink

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

Each 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

CapabilityConfigResponsibility
Transactional emailwebsiteConfig.emailVerification, reset password, magic links
NewsletterwebsiteConfig.newsletterMarketing 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 fromEmail uses 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.

On this page