Set Up React Email Templates for Cloud Projects

In this guide, you'll learn how to set up React Email templates for sending emails in Cloud projects.

What is React Email?#

React Email is a library that allows you to create email templates using React components. You can then transform these templates into HTML strings for sending emails.

React Email also allows you to preview and test your email templates during development.

In this guide, you'll learn how to:

  • Install React Email dependencies in your Medusa project.
  • Create an invite user email template using React Email.
  • Preview email templates during development.
  • Send emails using the React Email template with Medusa Emails.

You'll also find examples of common email templates built with React Email at the end of this guide.

Note: Medusa Emails is available for all Cloud organizations and projects, given you don't have another Notification Module Provider for the email channel configured. You also optionally need to verify your sending domain to send emails to users outside your organization.

Step 1: Install React Email Dependencies#

In the Medusa project where you want to set up email templates, install the following dependencies:

These packages include:

  • @react-email/components: Provides pre-built React components for building email templates.
  • @react-email/render: Renders React Email templates to HTML strings.
  • @react-email/preview-server and react-email: Development dependencies for previewing and testing email templates.
Note: Newer versions of React Email are not supported due to Node version incompatibilities on Cloud.

Step 2: Create Invite User Email Template#

Next, create the src/emails directory to store your email templates in your Medusa project.

Then, create your email templates as React components in that directory.

For example, to create an email template for sending invites to new admin users, create the file src/emails/invite-user.tsx with the following content:

src/emails/invite-user.tsx
1import {2  Text,3  Container,4  Heading,5  Html,6  Section,7  Tailwind,8  Head,9  Preview,10  Body,11  Button,12} from "@react-email/components"13
14type InviteEmailProps = {15  inviteUrl: string16  storeName: string17}18
19function Template({ inviteUrl, storeName }: InviteEmailProps) {20  return (21    <Tailwind>22      <Html className="font-sans bg-gray-100">23        <Head />24        <Preview>{`You've been invited to join ${storeName ?? ""}`}</Preview>25        <Body className="bg-white my-10 mx-auto w-full max-w-2xl">26          {/* Main Content */}27          <Container className="p-6 text-center">28            <Heading className="text-lg font-semibold text-black mb-6">29              You've been invited to join {storeName ?? ""}30            </Heading>31
32            <Text className="text-sm text-black leading-relaxed mb-6">33              Click the button below to accept the invitation and get started.34            </Text>35
36            <Section className="text-center my-8">37              <Button38                href={inviteUrl}39                className="bg-black text-white py-3 px-8 inline-block"40              >41                Accept invitation42              </Button>43            </Section>44          </Container>45
46          {/* Footer */}47          <Section className="bg-gray-50 p-6 mt-10">48            <Text className="text-center text-black text-xs mt-4">49              © {new Date().getFullYear()} {storeName}, Inc. All rights reserved.50            </Text>51          </Section>52        </Body>53      </Html>54    </Tailwind>55  )56}57
58export default function getInviteTemplate(props?: InviteEmailProps) {59  return (60    <Template 61      inviteUrl={props?.inviteUrl ?? "#"} 62      storeName={props?.storeName ?? "Demo Store"}63    />64  )65}

In the file, you define:

  • A React component Template that uses React Email components to structure the email.
  • A default export function getInviteTemplate that returns the Template component with default props.

The file must export a function that returns a React component representing the email template. React Email uses this function to preview the email, and you'll use it later to render the template to HTML.


Step 3: Preview Email Template (Optional)#

It's useful to preview your email templates during development to ensure they look as expected. This allows you to catch any design issues or mistakes early.

To preview your email template, use the React Email Preview Server.

First, add the following script to the scripts section of your package.json:

Code
1{2  "scripts": {3    "preview:emails": "email dev --dir ./src/emails"4  }5}

This script uses the email dev command from react-email, specifying the directory where your email templates are located.

Then, run the preview server with the following command:

The preview server will start at http://localhost:3000, where you can view and test your email templates in the browser.


Step 4: Send Email Using Template#

Once you have the email template, you can use it to send emails from your Medusa project deployed on Cloud.

For example, to send an invite email when a new admin user is invited, create a subscriber with the following content:

src/subscribers/invite-admin-subscriber.ts
1import type {2  SubscriberArgs,3  SubscriberConfig,4} from "@medusajs/framework"5import { render, pretty } from "@react-email/render"6import getInviteTemplate from "../emails/invite-user"7
8export default async function inviteCreatedHandler({9  event: { data },10  container,11}: SubscriberArgs<{ id: string }>) {12  const query = container.resolve("query")13
14  const {15    data: [store],16  } = await query.graph({17    entity: "store",18    fields: ["name"],19  })20
21  const {22    data: [invite],23  } = await query.graph({24    entity: "invite",25    fields: ["email", "token"],26    filters: {27      id: data.id,28    },29  })30
31  const config = container.resolve("configModule")32
33  const adminPath = config.admin.path34
35  const inviteUrl = `/${adminPath}/invite?token=${invite.token}`36
37  const notificationModule = container.resolve("notification")38
39  const html = await pretty(40    await render(getInviteTemplate({41      inviteUrl,42      storeName: store.name,43    }))44  )45
46  await notificationModule.createNotifications({47    to: invite.email,48    // optional: set from email49    // from: "no-reply@yourdomain.com"50    channel: "email",51    content: {52      html,53      subject: `You've been invited to join ${store.name}`,54    },55  })56}57
58export const config: SubscriberConfig = {59  event: [60    "invite.created",61    "invite.resend",62  ],63}

This subscriber listens for the invite.created and invite.resend events. When triggered, it:

  1. Retrieves the store name and invite details.
  2. Constructs the invite URL. Invites to the admin dashboard are at the /admin/invite path and include the invite token as a token query parameter.
  3. Renders the invite email template to an HTML string using React Email's render function, passing the getInviteTemplate function with the invite URL and store name as props.
  4. Sends the email by creating a notification using Medusa's Notification Module.

Test Email Sending on Cloud#

To test that your emails are sent correctly on Cloud, make sure you've pushed all changes to the project's repository, and the changes have been deployed to your Cloud project.

Then, invite a new admin user to your Cloud project through the Medusa Admin dashboard.

If everything is set up correctly, the new admin user should receive the invite email using your React Email template.

Otherwise, refer to the Troubleshooting Medusa Emails on Cloud guide for help resolving common issues.


Common Email Templates#

This section provides examples of common email templates you can create using React Email, then send in your Cloud projects. You can customize these templates to fit your brand and use case.

You can also refer to the React Email documentation for more information on building email templates with React Email.

Order Placed Email Template#

The Order Placed email template notifies customers when they successfully place an order. It typically includes order details, shipping information, and a thank-you message.

You can use the following template for your Order Placed email:

src/emails/order-placed.tsx
1import { CustomerDTO, OrderDTO } from "@medusajs/framework/types"2import {3  Body,4  Column,5  Container,6  Head,7  Heading,8  Html,9  Img,10  Preview,11  Row,12  Section,13  Tailwind,14  Text,15} from "@react-email/components"16
17type OrderPlacedEmailProps = {18  order: OrderDTO & { customer: CustomerDTO }19  storeName: string20}21
22function Template({ order, storeName }: OrderPlacedEmailProps) {23  const getLocaleAmount = (amount: number) => {24    const formatter = new Intl.NumberFormat([], {25      style: "currency",26      currencyDisplay: "narrowSymbol",27      currency: order.currency_code,28    })29
30    return formatter.format(amount)31  }32
33  const items = order.items?.map((item) => {34    const itemTotal = item.unit_price * item.quantity35
36    return (37      <Section key={item.id} className="border-b border-gray-200 py-4 px-0">38        <Row className="px-0">

Then, to send the Order Placed email when an order is created, create a subscriber at src/subscribers/order-created.ts with the following content:

src/subscribers/order-created.ts
1import type {2  SubscriberArgs,3  SubscriberConfig,4} from "@medusajs/framework"5import { render, pretty } from "@react-email/render"6import getOrderPlacedTemplate from "../emails/order-placed"7
8export default async function orderPlacedHandler({9  event: { data },10  container,11}: SubscriberArgs<{ id: string }>) {12  const query = container.resolve("query")13
14  const {15    data: [store],16  } = await query.graph({17    entity: "store",18    fields: ["name"],19  })20
21  const {22    data: [order],23  } = await query.graph({24    entity: "order",25    fields: [26      "id",27      "email",28      "display_id",29      "created_at",30
31      // Customer32      "customer.first_name",33      "customer.email",34
35      // Items36      "items.id",37      "items.product_title",38      "items.variant_title",39      "items.quantity",40      "items.unit_price",41
42      // Shipping address43      "shipping_address.first_name",44      "shipping_address.last_name",45      "shipping_address.address_1",46      "shipping_address.address_2",47      "shipping_address.city",48      "shipping_address.postal_code",49      "shipping_address.country_code",50
51      // Totals52      "item_total",53      "shipping_total",54      "discount_total",55      "tax_total",56      "total",57    ],58    filters: {59      id: data.id,60    },61  })62
63  if (!order.customer?.email) {64    console.warn(`Order ${order.id} has no customer email, skipping notification`)65    return66  }67
68  const notificationModule = container.resolve("notification")69
70  const html = await pretty(71    await render(getOrderPlacedTemplate({72      order: order as any,73      storeName: store.name,74    }))75  )76
77  await notificationModule.createNotifications({78    to: order.customer.email,79    // optional: set from email80    // from: "no-reply@yourdomain.com"81    channel: "email",82    content: {83      html,84      subject: `Order Confirmation - ${store.name}`,85    },86  })87}88
89export const config: SubscriberConfig = {90  event: "order.placed",91}

This subscriber listens for the order.placed event. When triggered, it:

  1. Retrieves the store name and order details.
  2. Renders the Order Placed email template to an HTML string using React Email's render function, passing the getOrderPlacedTemplate function with the order and store name as props.
  3. Sends the email by creating a notification using Medusa's Notification Module.

Whenever a new order is placed, the customer will receive an Order Placed email using your React Email template.

Reset Password Email Template#

The Reset Password email template allows customers and admin users to reset their password if they forget it. It typically includes a link to a password reset page.

You can use the following template for your Reset Password email:

src/emails/reset-password.tsx
1import {2  Text,3  Container,4  Heading,5  Html,6  Section,7  Tailwind,8  Head,9  Preview,10  Body,11  Button,12} from "@react-email/components"13
14type ResetPasswordEmailProps = {15  resetPasswordUrl: string16  storeName: string17}18
19function Template({ resetPasswordUrl, storeName }: ResetPasswordEmailProps) {20  return (21    <Tailwind>22      <Html className="font-sans bg-gray-100">23        <Head />24        <Preview>Reset your password</Preview>25        <Body className="bg-white my-10 mx-auto w-full max-w-2xl">26          {/* Main Content */}27          <Container className="p-6 text-center">28            <Heading className="text-lg font-semibold text-black mb-6">29              You have submitted a password change request.30            </Heading>31
32            <Section className="text-center my-8">33              <Button34                href={resetPasswordUrl}35                className="bg-black text-white py-3 px-8 inline-block"36              >37                Reset password38              </Button>39            </Section>40
41            <Text className="text-sm text-black leading-relaxed mt-6">42              If you didn't request a password reset, you can safely ignore this43              email.44            </Text>45          </Container>46
47          {/* Footer */}48          <Section className="bg-gray-50 p-6 mt-10">49            <Text className="text-center text-black text-xs mt-4">50              © {new Date().getFullYear()} {storeName}, Inc. All rights reserved.51            </Text>52          </Section>53        </Body>54      </Html>55    </Tailwind>56  )57}58
59export default function getResetPasswordTemplate(props?: ResetPasswordEmailProps) {60  return (61    <Template62      resetPasswordUrl={props?.resetPasswordUrl ?? "#"}63      storeName={props?.storeName ?? "Demo Store"}64    />65  )66}

Then, to send the Reset Password email when a password reset is requested, create a subscriber at src/subscribers/reset-password.ts with the following content:

src/subscribers/reset-password.ts
1import {2  SubscriberArgs,3  type SubscriberConfig,4} from "@medusajs/medusa"5import { Modules } from "@medusajs/framework/utils"6import { render, pretty } from "@react-email/render"7import getResetPasswordTemplate from "../emails/reset-password"8
9export default async function resetPasswordTokenHandler({10  event: { data: {11    entity_id: email,12    token,13    actor_type,14  } },15  container,16}: SubscriberArgs<{ entity_id: string, token: string, actor_type: string }>) {17  const notificationModuleService = container.resolve(18    Modules.NOTIFICATION19  )20  const config = container.resolve("configModule")21  const query = container.resolve("query")22
23  const {24    data: [store],25  } = await query.graph({26    entity: "store",27    fields: ["name"],28  })29
30  let urlPrefix = ""31
32  if (actor_type === "customer") {33    urlPrefix = config.admin.storefrontUrl || "https://storefront.com"34  } else {35    const backendUrl = config.admin.backendUrl !== "/" ? config.admin.backendUrl :36      "http://localhost:9000"37    const adminPath = config.admin.path38    urlPrefix = `${backendUrl}${adminPath}`39  }40
41  const resetPasswordUrl = `${urlPrefix}/reset-password?token=${token}&email=${email}`42
43  const html = await pretty(44    await render(getResetPasswordTemplate({45      resetPasswordUrl,46      storeName: store.name,47    }))48  )49
50  await notificationModuleService.createNotifications({51    to: email,52    // optional: set from email53    // from: "no-reply@yourdomain.com"54    channel: "email",55    content: {56      html,57      subject: `Reset your password - ${store.name}`,58    },59  })60}61
62export const config: SubscriberConfig = {63  event: "auth.password_reset",64}

This subscriber listens for the auth.password_reset event. When triggered, it:

  1. Retrieves the store name.
  2. Constructs the reset password URL based on whether the request is for a customer or admin user.
  3. Renders the Reset Password email template to an HTML string using React Email's render function, passing the getResetPasswordTemplate function with the reset password URL and store name as props.
  4. Sends the email by creating a notification using Medusa's Notification Module.

Whenever a user requests a password reset, they will receive a Reset Password email using your React Email template.

Was this guide helpful?
Ask Anything
Ask any questions about Medusa. Get help with your development.
You can also use the Medusa MCP server in Cursor, VSCode, etc...
FAQ
What is Medusa?
How can I create a module?
How can I create a data model?
How do I create a workflow?
How can I extend a data model in the Product Module?
Recipes
How do I build a marketplace with Medusa?
How do I build digital products with Medusa?
How do I build subscription-based purchases with Medusa?
What other recipes are available in the Medusa documentation?
Chat is cleared on refresh
Line break