docs: emails in cloud (#14040)
* docs: emails in cloud * add notes to user guides * small updates * small change * fix link * addressed feedback
This commit is contained in:
8
www/apps/cloud/app/cache/page.mdx
vendored
8
www/apps/cloud/app/cache/page.mdx
vendored
@@ -35,8 +35,8 @@ To enable Medusa Cache for your Medusa project deployed on Cloud, set the `cachi
|
||||
module.exports = defineConfig({
|
||||
// ...
|
||||
featureFlags: {
|
||||
caching: true
|
||||
}
|
||||
caching: true,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
@@ -70,8 +70,8 @@ const { data: products } = await query.graph({
|
||||
fields: ["id", "title", "handle"],
|
||||
}, {
|
||||
cache: {
|
||||
enable: true
|
||||
}
|
||||
enable: true,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
311
www/apps/cloud/app/emails/page.mdx
Normal file
311
www/apps/cloud/app/emails/page.mdx
Normal file
@@ -0,0 +1,311 @@
|
||||
import { CodeTabs, CodeTab, Prerequisites, InlineIcon } from "docs-ui"
|
||||
import { ChevronUpDown } from "@medusajs/icons"
|
||||
|
||||
export const metadata = {
|
||||
title: `Medusa Emails`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn about Medusa Emails and how to send emails in your Cloud projects.
|
||||
|
||||
## What is Medusa Emails?
|
||||
|
||||
Medusa Emails is a built-in email sending service for Cloud organizations and projects.
|
||||
|
||||
<Note title="Not a Cloud user yet?">
|
||||
|
||||
[Sign up for Cloud](../sign-up/page.mdx) to use Medusa Emails and other Cloud features.
|
||||
|
||||
</Note>
|
||||
|
||||
Medusa Emails allows you to send transactional and marketing emails like order confirmations directly from your Medusa application without needing to set up and manage your own email infrastructure.
|
||||
|
||||
Medusa Emails is available for all Cloud plans, with different sending limits based on your [plan](../pricing/page.mdx). You can use it out-of-the-box without manual configuration.
|
||||
|
||||
Through your organization's dashboard, you can also monitor email sending statistics and manage your email sender domain.
|
||||
|
||||
---
|
||||
|
||||
## Set Up Medusa Emails in Cloud Projects
|
||||
|
||||
Medusa Emails is enabled by default for all Cloud projects and organizations with zero configuration.
|
||||
|
||||
### Remove Other Email Notification Module Providers
|
||||
|
||||
Medusa enables Medusa Emails by default for all Cloud projects if you haven't configured a [Notification Module Provider](!resources!/infrastructure-modules/notification) for the `email` channel in your Medusa application.
|
||||
|
||||
So, if you have a notification provider like the [SendGrid Notification Module Provider](!resources!/infrastructure-modules/notification/sendgrid), remove it from your Medusa application to use Medusa Emails.
|
||||
|
||||
#### Local Development Email Setup
|
||||
|
||||
Since Medusa Emails isn't available for local development, you can set up the [Local Notification Module Provider](!resources!/infrastructure-modules/notification/local) to test email sending functionality in your local environment. This provider will log the email details to the console instead of sending them.
|
||||
|
||||
For example, you can add the following configuration to `medusa-config.ts`:
|
||||
|
||||
```ts title="medusa-config.ts"
|
||||
module.exports = defineConfig({
|
||||
// ...
|
||||
modules: [
|
||||
{
|
||||
resolve: "@medusajs/medusa/notification",
|
||||
options: {
|
||||
providers: [
|
||||
// ...
|
||||
{
|
||||
resolve: "@medusajs/medusa/notification-local",
|
||||
id: "local",
|
||||
options: {
|
||||
channels: [
|
||||
process.env.NODE_ENV === "development"
|
||||
? "email"
|
||||
: "feed",
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
In this setup, the Local Notification Module Provider is used for the `email` channel only in the development environment. In other environments, it uses the `feed` channel.
|
||||
|
||||
### (Optional) Verify Your Email Sender Domain
|
||||
|
||||
By default, Medusa Emails sends emails using a randomly generated email address at the domain `medusajsemails.com`.
|
||||
|
||||
If you don't verify an email sender domain, you can only send emails to addresses of users in your Cloud organization.
|
||||
|
||||
To send emails to external email addresses, [verify your email sender domain](#verify-email-sender-domain-on-cloud).
|
||||
|
||||
---
|
||||
|
||||
## Send Emails on Cloud
|
||||
|
||||
To send emails using Medusa Emails in your Cloud project, create a notification that's sent through the `email` channel in your project's code.
|
||||
|
||||
For example:
|
||||
|
||||
<CodeTabs group="medusa-email-example">
|
||||
<CodeTab label="Workflow" value="workflow">
|
||||
|
||||
```ts
|
||||
import { createWorkflow } from "@medusajs/framework/workflows-sdk"
|
||||
import { sendNotificationsStep } from "@medusajs/medusa/core-flows"
|
||||
|
||||
type Input = {
|
||||
email: string
|
||||
}
|
||||
|
||||
const myWorkflow = createWorkflow(
|
||||
"my-workflow",
|
||||
(input: Input) => {
|
||||
const data = sendNotificationsStep({
|
||||
// must either have a verified sender domain or use an email
|
||||
// address that belongs to your Cloud organization
|
||||
to: input.email,
|
||||
channel: "email",
|
||||
content: {
|
||||
html: "<h1>Welcome to Medusa Emails!</h1><p>This is a test email sent using Medusa Emails in a Cloud project.</p>",
|
||||
subject: "Welcome to Medusa Emails",
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
<CodeTab label="Scheduled Job / Subscriber / API Route" value="job">
|
||||
|
||||
```ts
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
|
||||
// ...
|
||||
|
||||
const notificationModuleService = container.resolve(
|
||||
Modules.NOTIFICATION
|
||||
)
|
||||
|
||||
await notificationModuleService.createNotifications({
|
||||
// must either have a verified sender domain or use an email
|
||||
// address that belongs to your Cloud organization
|
||||
to: "user@example.com",
|
||||
channel: "email",
|
||||
content: {
|
||||
html: "<h1>Welcome to Medusa Emails!</h1><p>This is a test email sent using Medusa Emails in a Cloud project.</p>",
|
||||
subject: "Welcome to Medusa Emails",
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
|
||||
When this code is executed in your Cloud project, Medusa sends the email using Medusa Emails. You can track whether the email was delivered on the [Email Activity page](#monitor-email-sending-activity-on-cloud).
|
||||
|
||||
### Writing Email Templates
|
||||
|
||||
To write email templates for Medusa Emails, use packages like [react-email](https://react.email). React Email lets you create email templates using React components with preview and testing capabilities.
|
||||
|
||||
Refer to the [Set Up React Email Templates](./react-email/page.mdx) guide to learn how to set up React Email templates in your Cloud project with examples.
|
||||
|
||||
---
|
||||
|
||||
## Verify Email Sender Domain on Cloud
|
||||
|
||||
<Prerequisites
|
||||
items={[
|
||||
{
|
||||
text: "A domain that you own and can access its DNS settings.",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
To send emails to external email addresses using Medusa Emails, you need to verify your email sender domain in your Cloud organization. You can verify multiple sender domains if needed.
|
||||
|
||||
To verify your email sender domain:
|
||||
|
||||
1. Make sure you're viewing the [correct organization's dashboard in Cloud](../organizations/page.mdx#switch-organization).
|
||||
2. Click on the <InlineIcon Icon={ChevronUpDown} alt="switch organization" /> icon in the [organization switcher](../organizations/page.mdx#switch-organization) at the top left of the Cloud dashboard.
|
||||
3. Choose "Organization Settings" from the dropdown.
|
||||
|
||||

|
||||
|
||||
4. Click Emails -> Sender Domains from the sidebar.
|
||||
5. Click the "Add sender domain" button.
|
||||
6. In the form that opens, perform these two steps:
|
||||
- **Domain**: Enter the domain you want to verify (for example, `yourdomain.com`).
|
||||
- **DNS Records**: Add the provided DNS records to your domain's DNS settings. These records are necessary for verifying domain ownership and setting up email authentication.
|
||||
- If you're unsure how to add DNS records, refer to your domain registrar's documentation or support.
|
||||
7. Once you're done, click the "I've added the records" button.
|
||||
|
||||
The domain will then be added, and the system will periodically check the DNS records to verify it. This process may take a few minutes to several hours, depending on your DNS provider.
|
||||
|
||||

|
||||
|
||||
### Specify From Email
|
||||
|
||||
After verifying your email sender domain, specify a `from` property in the notification to set the sender email address.
|
||||
|
||||
For example:
|
||||
|
||||
<CodeTabs group="medusa-email-from-example">
|
||||
<CodeTab label="Workflow" value="workflow">
|
||||
|
||||
```ts
|
||||
import { createWorkflow } from "@medusajs/framework/workflows-sdk"
|
||||
import { sendNotificationsStep } from "@medusajs/medusa/core-flows"
|
||||
|
||||
type Input = {
|
||||
email: string
|
||||
}
|
||||
|
||||
const myWorkflow = createWorkflow(
|
||||
"my-workflow",
|
||||
(input: Input) => {
|
||||
const data = sendNotificationsStep({
|
||||
to: input.email,
|
||||
from: "no-reply@yourdomain.com",
|
||||
channel: "email",
|
||||
content: {
|
||||
html: "<h1>Welcome to Medusa Emails!</h1><p>This is a test email sent using Medusa Emails in a Cloud project.</p>",
|
||||
subject: "Welcome to Medusa Emails",
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
<CodeTab label="Scheduled Job / Subscriber / API Route" value="job">
|
||||
|
||||
```ts
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
|
||||
// ...
|
||||
|
||||
const notificationModuleService = container.resolve(
|
||||
Modules.NOTIFICATION
|
||||
)
|
||||
|
||||
await notificationModuleService.createNotifications({
|
||||
to: "user@example.com",
|
||||
from: "no-reply@yourdomain.com",
|
||||
channel: "email",
|
||||
content: {
|
||||
html: "<h1>Welcome to Medusa Emails!</h1><p>This is a test email sent using Medusa Emails in a Cloud project.</p>",
|
||||
subject: "Welcome to Medusa Emails",
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
|
||||
The email will be sent from the `no-reply@yourdomain.com` address, assuming it belongs to your verified sender domain.
|
||||
|
||||
---
|
||||
|
||||
## Monitor Email Sending Activity on Cloud
|
||||
|
||||
You can monitor email sending activity like sent emails, delivery rates, open rates, and click rates for your Cloud organization using the Email Activity page in your Cloud dashboard.
|
||||
|
||||
To view email sending activity:
|
||||
|
||||
1. Make sure you're viewing the [correct organization's dashboard in Cloud](../organizations/page.mdx#switch-organization).
|
||||
2. Click on the <InlineIcon Icon={ChevronUpDown} alt="switch organization" /> icon in the [organization switcher](../organizations/page.mdx#switch-organization) at the top left of the Cloud dashboard.
|
||||
3. Choose "Organization Settings" from the dropdown.
|
||||
4. Click Emails -> Activity from the sidebar.
|
||||
|
||||
At the top of the Email Activity page, you'll see email sending statistics for your organization across all projects, including:
|
||||
|
||||
- **Sent**: Total number of sent emails in the specified time range.
|
||||
- **Deliverability Rate**: Percentage of sent emails that were successfully delivered to recipients.
|
||||
- **Open Rate**: Percentage of delivered emails that were opened by recipients.
|
||||
- **Click Rate**: Percentage of opened emails where recipients clicked on links within the email.
|
||||
- **Bounce Rate**: Percentage of sent emails that were not delivered due to permanent delivery failures.
|
||||
- **Complaint Rate**: Percentage of sent emails that recipients marked as spam or junk.
|
||||
|
||||
Below the statistics, you'll find a detailed list of sent emails.
|
||||
|
||||

|
||||
|
||||
### Change Email Activity Time Range
|
||||
|
||||
To change the time range for email activity statistics and the list of sent emails, click the time range dropdown at the top right of the page. You can choose custom ranges or predefined ranges like "Last 7 days" or "Last 30 days".
|
||||
|
||||

|
||||
|
||||
### Filter Emails by Project
|
||||
|
||||
By default, the Email Activity page shows emails from all projects in your Cloud organization.
|
||||
|
||||
To filter emails by a specific project, click the "All projects" dropdown at the top right of the page and select the desired project.
|
||||
|
||||
This updates the email sending statistics and list to show only emails from the selected project.
|
||||
|
||||

|
||||
|
||||
### View Email Details
|
||||
|
||||
To view an email's details, click the email entry in the list on the Email Activity page.
|
||||
|
||||
At the top of the Email Details page, you'll see an overview of the email's event history, including when it was sent, delivered, opened, and when its links were clicked.
|
||||
|
||||
Below that, you'll see a preview of the email content as it was sent to the recipient, with support for both HTML and plain text views.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Troubleshoot Email Delivery Issues on Cloud
|
||||
|
||||
If you try to send an email using Medusa Emails in your Cloud project and it's not delivered, consider these troubleshooting steps:
|
||||
|
||||
1. **Check Email Activity**: Go to the [Email Activity page](#monitor-email-sending-activity-on-cloud) in your Cloud organization's dashboard to see if the email appears in the list. If it does, check its status and event history for delivery issues.
|
||||
2. **Verify Sender Domain**: If you're sending emails to external addresses, ensure you've verified your email sender domain in your Cloud organization. Without a verified sender domain, you can only send emails to addresses in your Cloud organization.
|
||||
3. **Check Logs**: Review your project's [logs](../logs/page.mdx#view-runtime-logs-on-cloud) for errors or warnings related to email sending. Look for issues that might indicate why the email wasn't sent or delivered.
|
||||
4. **Check Spam/Junk Folder**: Ask the recipient to check their spam or junk email folder. Sometimes emails may be incorrectly marked as spam by email providers.
|
||||
5. **Contact Support**: If you've tried the above steps and are still experiencing issues, contact support for assistance.
|
||||
790
www/apps/cloud/app/emails/react-email/page.mdx
Normal file
790
www/apps/cloud/app/emails/react-email/page.mdx
Normal file
@@ -0,0 +1,790 @@
|
||||
import { Prerequisites } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `Set Up React Email Templates for Cloud Projects`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this guide, you'll learn how to set up React Email templates for [sending emails in Cloud projects](../page.mdx).
|
||||
|
||||
## What is React Email?
|
||||
|
||||
[React Email](https://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](../page.mdx#remove-other-email-notification-module-providers). You also optionally need to [verify your sending domain](../page.mdx#verify-email-sender-domain-on-cloud) to send emails to users outside your organization.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Install React Email Dependencies
|
||||
|
||||
In the Medusa project where you want to set up email templates, install the following dependencies:
|
||||
|
||||
```bash npm2yarn
|
||||
npm install @react-email/components@0.3.3 @react-email/render@1.1.4
|
||||
npm install -D @react-email/preview-server@4.2.5 react-email@4.2.5
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## 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:
|
||||
|
||||
```tsx title="src/emails/invite-user.tsx"
|
||||
import {
|
||||
Text,
|
||||
Container,
|
||||
Heading,
|
||||
Html,
|
||||
Section,
|
||||
Tailwind,
|
||||
Head,
|
||||
Preview,
|
||||
Body,
|
||||
Button,
|
||||
} from "@react-email/components"
|
||||
|
||||
type InviteEmailProps = {
|
||||
inviteUrl: string
|
||||
storeName: string
|
||||
}
|
||||
|
||||
function Template({ inviteUrl, storeName }: InviteEmailProps) {
|
||||
return (
|
||||
<Tailwind>
|
||||
<Html className="font-sans bg-gray-100">
|
||||
<Head />
|
||||
<Preview>{`You've been invited to join ${storeName ?? ""}`}</Preview>
|
||||
<Body className="bg-white my-10 mx-auto w-full max-w-2xl">
|
||||
{/* Main Content */}
|
||||
<Container className="p-6 text-center">
|
||||
<Heading className="text-lg font-semibold text-black mb-6">
|
||||
You've been invited to join {storeName ?? ""}
|
||||
</Heading>
|
||||
|
||||
<Text className="text-sm text-black leading-relaxed mb-6">
|
||||
Click the button below to accept the invitation and get started.
|
||||
</Text>
|
||||
|
||||
<Section className="text-center my-8">
|
||||
<Button
|
||||
href={inviteUrl}
|
||||
className="bg-black text-white py-3 px-8 inline-block"
|
||||
>
|
||||
Accept invitation
|
||||
</Button>
|
||||
</Section>
|
||||
</Container>
|
||||
|
||||
{/* Footer */}
|
||||
<Section className="bg-gray-50 p-6 mt-10">
|
||||
<Text className="text-center text-black text-xs mt-4">
|
||||
© {new Date().getFullYear()} {storeName}, Inc. All rights reserved.
|
||||
</Text>
|
||||
</Section>
|
||||
</Body>
|
||||
</Html>
|
||||
</Tailwind>
|
||||
)
|
||||
}
|
||||
|
||||
export default function getInviteTemplate(props?: InviteEmailProps) {
|
||||
return (
|
||||
<Template
|
||||
inviteUrl={props?.inviteUrl ?? "#"}
|
||||
storeName={props?.storeName ?? "Demo Store"}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
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`:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"preview:emails": "email dev --dir ./src/emails"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run preview:emails
|
||||
```
|
||||
|
||||
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](!docs!/learn/fundamentals/events-and-subscribers) with the following content:
|
||||
|
||||
```ts title="src/subscribers/invite-admin-subscriber.ts"
|
||||
import type {
|
||||
SubscriberArgs,
|
||||
SubscriberConfig,
|
||||
} from "@medusajs/framework"
|
||||
import { render, pretty } from "@react-email/render"
|
||||
import getInviteTemplate from "../emails/invite-user"
|
||||
|
||||
export default async function inviteCreatedHandler({
|
||||
event: { data },
|
||||
container,
|
||||
}: SubscriberArgs<{ id: string }>) {
|
||||
const query = container.resolve("query")
|
||||
|
||||
const {
|
||||
data: [store],
|
||||
} = await query.graph({
|
||||
entity: "store",
|
||||
fields: ["name"],
|
||||
})
|
||||
|
||||
const {
|
||||
data: [invite],
|
||||
} = await query.graph({
|
||||
entity: "invite",
|
||||
fields: ["email", "token"],
|
||||
filters: {
|
||||
id: data.id,
|
||||
},
|
||||
})
|
||||
|
||||
const config = container.resolve("configModule")
|
||||
|
||||
const adminPath = config.admin.path
|
||||
|
||||
const inviteUrl = `/${adminPath}/invite?token=${invite.token}`
|
||||
|
||||
const notificationModule = container.resolve("notification")
|
||||
|
||||
const html = await pretty(
|
||||
await render(getInviteTemplate({
|
||||
inviteUrl,
|
||||
storeName: store.name,
|
||||
}))
|
||||
)
|
||||
|
||||
await notificationModule.createNotifications({
|
||||
to: invite.email,
|
||||
// optional: set from email
|
||||
// from: "no-reply@yourdomain.com"
|
||||
channel: "email",
|
||||
content: {
|
||||
html,
|
||||
subject: `You've been invited to join ${store.name}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const config: SubscriberConfig = {
|
||||
event: [
|
||||
"invite.created",
|
||||
"invite.resend",
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
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](!user-guide!/settings/users/invites) 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](../page.mdx#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](https://react.email/docs) 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:
|
||||
|
||||
```tsx title="src/emails/order-placed.tsx" collapsibleLines="36-257"
|
||||
import { CustomerDTO, OrderDTO } from "@medusajs/framework/types"
|
||||
import {
|
||||
Body,
|
||||
Column,
|
||||
Container,
|
||||
Head,
|
||||
Heading,
|
||||
Html,
|
||||
Img,
|
||||
Preview,
|
||||
Row,
|
||||
Section,
|
||||
Tailwind,
|
||||
Text,
|
||||
} from "@react-email/components"
|
||||
|
||||
type OrderPlacedEmailProps = {
|
||||
order: OrderDTO & { customer: CustomerDTO }
|
||||
storeName: string
|
||||
}
|
||||
|
||||
function Template({ order, storeName }: OrderPlacedEmailProps) {
|
||||
const getLocaleAmount = (amount: number) => {
|
||||
const formatter = new Intl.NumberFormat([], {
|
||||
style: "currency",
|
||||
currencyDisplay: "narrowSymbol",
|
||||
currency: order.currency_code,
|
||||
})
|
||||
|
||||
return formatter.format(amount)
|
||||
}
|
||||
|
||||
const items = order.items?.map((item) => {
|
||||
const itemTotal = item.unit_price * item.quantity
|
||||
|
||||
return (
|
||||
<Section key={item.id} className="border-b border-gray-200 py-4 px-0">
|
||||
<Row className="px-0">
|
||||
<Column className="w-1/6">
|
||||
<Img
|
||||
src={item.thumbnail ?? ""}
|
||||
alt={""}
|
||||
className="rounded-lg"
|
||||
width="100%"
|
||||
/>
|
||||
</Column>
|
||||
<Column className="w-7/8 pl-4">
|
||||
<Text className="text-sm text-black my-2">
|
||||
{item.product_title}
|
||||
</Text>
|
||||
<Text className="text-xs text-black my-2">
|
||||
{item.variant_title}
|
||||
</Text>
|
||||
<Text className="text-xs text-black my-2">
|
||||
<span className="font-semibold">
|
||||
{item.quantity} x {getLocaleAmount(item.unit_price)}
|
||||
</span>
|
||||
</Text>
|
||||
<Text className="text-xs text-black font-bold my-2">
|
||||
{getLocaleAmount(itemTotal)}
|
||||
</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
</Section>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<Tailwind>
|
||||
<Html className="font-sans bg-gray-100">
|
||||
<Head />
|
||||
<Preview>Thank you for your order from {storeName}</Preview>
|
||||
<Body className="bg-white my-10 mx-auto w-full max-w-2xl">
|
||||
{/* Greeting and Order Confirmation */}
|
||||
<Container className="p-6">
|
||||
<Heading className="text-lg font-normal text-black mb-6">
|
||||
Dear{" "}
|
||||
{order.customer?.first_name || order.shipping_address?.first_name}
|
||||
,
|
||||
</Heading>
|
||||
|
||||
<Text className="text-sm text-black leading-relaxed mb-6">
|
||||
Thank you for your order. Below you will find the details for your
|
||||
purchase.
|
||||
</Text>
|
||||
|
||||
<div className="mb-6">
|
||||
<Text className="text-sm text-black m-0 mb-1">
|
||||
Order number:{" "}
|
||||
<span className="font-semibold">{order.display_id}</span>
|
||||
</Text>
|
||||
<Text className="text-sm text-black m-0">
|
||||
Order date:{" "}
|
||||
<span className="font-semibold">
|
||||
{new Date(order.created_at).toLocaleDateString("en-GB", {
|
||||
weekday: "short",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})}
|
||||
</span>
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Text className="text-sm text-black leading-relaxed">
|
||||
We're getting your order ready to be shipped. We will notify you
|
||||
when it has been sent. If you have any questions, please don't
|
||||
hesitate to contact us.
|
||||
</Text>
|
||||
</Container>
|
||||
|
||||
{/* Order Items */}
|
||||
<Container className="px-6">
|
||||
<Heading className="text-base font-semibold text-black mb-2">
|
||||
Your Items
|
||||
</Heading>
|
||||
|
||||
{items}
|
||||
|
||||
{/* Order Summary */}
|
||||
<Section className="mt-8 border-t border-gray-200 pt-6">
|
||||
<Row className="text-black">
|
||||
<Column className="w-1/2">
|
||||
<Text className="text-sm m-0 mb-2">Subtotal (incl. VAT)</Text>
|
||||
</Column>
|
||||
<Column className="w-1/2 text-right">
|
||||
<Text className="text-sm m-0 mb-2">
|
||||
{getLocaleAmount(order.item_total as number)}
|
||||
</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
<Row className="text-black">
|
||||
<Column className="w-1/2">
|
||||
<Text className="text-sm m-0 mb-2">Shipping Total</Text>
|
||||
</Column>
|
||||
<Column className="w-1/2 text-right">
|
||||
<Text className="text-sm m-0 mb-2">
|
||||
{getLocaleAmount(order.shipping_total as number)}
|
||||
</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
{Number(order.discount_total) > 0 ? (
|
||||
<Row className="text-black">
|
||||
<Column className="w-1/2">
|
||||
<Text className="text-sm m-0 mb-2">Discount</Text>
|
||||
</Column>
|
||||
<Column className="w-1/2 text-right">
|
||||
<Text className="text-sm m-0 mb-2">
|
||||
-{getLocaleAmount(order.discount_total as number)}
|
||||
</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
) : null}
|
||||
<Row className="text-black font-bold">
|
||||
<Column className="w-1/2">
|
||||
<Text className="text-sm m-0 mb-2">Total</Text>
|
||||
</Column>
|
||||
<Column className="w-1/2 text-right">
|
||||
<Text className="text-sm m-0 mb-2">
|
||||
{getLocaleAmount(order.total as number)}
|
||||
</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
<Row className="text-black">
|
||||
<Column className="w-1/2">
|
||||
<Text className="text-sm m-0 italic">VAT Amount</Text>
|
||||
</Column>
|
||||
<Column className="w-1/2 text-right">
|
||||
<Text className="text-sm m-0 italic">
|
||||
{getLocaleAmount(order.tax_total as number)}
|
||||
</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
</Section>
|
||||
|
||||
{/* Shipping Address */}
|
||||
<Section className="mt-8 mb-8">
|
||||
<Heading className="text-base font-semibold text-black mb-2">
|
||||
Shipping Address
|
||||
</Heading>
|
||||
<Text className="text-sm text-black m-0 mb-1">
|
||||
{order.shipping_address?.first_name}{" "}
|
||||
{order.shipping_address?.last_name}
|
||||
</Text>
|
||||
<Text className="text-sm text-black m-0 mb-1">
|
||||
{order.shipping_address?.address_1}
|
||||
</Text>
|
||||
{order.shipping_address?.address_2 && (
|
||||
<Text className="text-sm text-black m-0 mb-1">
|
||||
{order.shipping_address?.address_2}
|
||||
</Text>
|
||||
)}
|
||||
<Text className="text-sm text-black m-0 mb-1">
|
||||
{order.shipping_address?.postal_code}{" "}
|
||||
{order.shipping_address?.city}
|
||||
</Text>
|
||||
<Text className="text-sm text-black m-0">
|
||||
{order.shipping_address?.country_code?.toUpperCase()}
|
||||
</Text>
|
||||
</Section>
|
||||
</Container>
|
||||
|
||||
{/* Footer */}
|
||||
<Section className="bg-gray-50 p-6 mt-10">
|
||||
<Text className="text-center text-black text-sm">
|
||||
Order ID: {order.id}
|
||||
</Text>
|
||||
<Text className="text-center text-black text-xs mt-4">
|
||||
© {new Date().getFullYear()} {storeName}, Inc. All rights reserved.
|
||||
</Text>
|
||||
</Section>
|
||||
</Body>
|
||||
</Html>
|
||||
</Tailwind>
|
||||
)
|
||||
}
|
||||
|
||||
export default function getOrderPlacedTemplate(props?: OrderPlacedEmailProps) {
|
||||
const demoData: OrderDTO & { customer: CustomerDTO } = {
|
||||
id: "order_01J5K6M8N9P0Q1R2S3T4U5V6W7",
|
||||
display_id: 1234,
|
||||
created_at: new Date().toISOString(),
|
||||
currency_code: "usd",
|
||||
item_total: 5999,
|
||||
shipping_total: 500,
|
||||
discount_total: 0,
|
||||
tax_total: 840,
|
||||
total: 6499,
|
||||
items: [
|
||||
{
|
||||
id: "item_01",
|
||||
product_title: "Premium Wireless Headphones",
|
||||
variant_title: "Black / Standard",
|
||||
thumbnail: "https://images.unsplash.com/photo-1618366712010-f4ae9c647dcb",
|
||||
unit_price: 2999,
|
||||
quantity: 2,
|
||||
},
|
||||
],
|
||||
customer: {
|
||||
id: "cust_01",
|
||||
email: "customer@example.com",
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
},
|
||||
shipping_address: {
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
address_1: "123 Main Street",
|
||||
address_2: "Apt 4B",
|
||||
city: "New York",
|
||||
postal_code: "10001",
|
||||
country_code: "us",
|
||||
},
|
||||
} as OrderDTO & { customer: CustomerDTO }
|
||||
|
||||
return <Template order={props?.order ?? demoData} storeName={props?.storeName ?? "Demo Store"} />
|
||||
}
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```ts title="src/subscribers/order-created.ts"
|
||||
import type {
|
||||
SubscriberArgs,
|
||||
SubscriberConfig,
|
||||
} from "@medusajs/framework"
|
||||
import { render, pretty } from "@react-email/render"
|
||||
import getOrderPlacedTemplate from "../emails/order-placed"
|
||||
|
||||
export default async function orderPlacedHandler({
|
||||
event: { data },
|
||||
container,
|
||||
}: SubscriberArgs<{ id: string }>) {
|
||||
const query = container.resolve("query")
|
||||
|
||||
const {
|
||||
data: [store],
|
||||
} = await query.graph({
|
||||
entity: "store",
|
||||
fields: ["name"],
|
||||
})
|
||||
|
||||
const {
|
||||
data: [order],
|
||||
} = await query.graph({
|
||||
entity: "order",
|
||||
fields: [
|
||||
"id",
|
||||
"email",
|
||||
"display_id",
|
||||
"created_at",
|
||||
|
||||
// Customer
|
||||
"customer.first_name",
|
||||
"customer.email",
|
||||
|
||||
// Items
|
||||
"items.id",
|
||||
"items.product_title",
|
||||
"items.variant_title",
|
||||
"items.quantity",
|
||||
"items.unit_price",
|
||||
|
||||
// Shipping address
|
||||
"shipping_address.first_name",
|
||||
"shipping_address.last_name",
|
||||
"shipping_address.address_1",
|
||||
"shipping_address.address_2",
|
||||
"shipping_address.city",
|
||||
"shipping_address.postal_code",
|
||||
"shipping_address.country_code",
|
||||
|
||||
// Totals
|
||||
"item_total",
|
||||
"shipping_total",
|
||||
"discount_total",
|
||||
"tax_total",
|
||||
"total",
|
||||
],
|
||||
filters: {
|
||||
id: data.id,
|
||||
},
|
||||
})
|
||||
|
||||
if (!order.customer?.email) {
|
||||
console.warn(`Order ${order.id} has no customer email, skipping notification`)
|
||||
return
|
||||
}
|
||||
|
||||
const notificationModule = container.resolve("notification")
|
||||
|
||||
const html = await pretty(
|
||||
await render(getOrderPlacedTemplate({
|
||||
order: order as any,
|
||||
storeName: store.name,
|
||||
}))
|
||||
)
|
||||
|
||||
await notificationModule.createNotifications({
|
||||
to: order.customer.email,
|
||||
// optional: set from email
|
||||
// from: "no-reply@yourdomain.com"
|
||||
channel: "email",
|
||||
content: {
|
||||
html,
|
||||
subject: `Order Confirmation - ${store.name}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const config: SubscriberConfig = {
|
||||
event: "order.placed",
|
||||
}
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```tsx title="src/emails/reset-password.tsx"
|
||||
import {
|
||||
Text,
|
||||
Container,
|
||||
Heading,
|
||||
Html,
|
||||
Section,
|
||||
Tailwind,
|
||||
Head,
|
||||
Preview,
|
||||
Body,
|
||||
Button,
|
||||
} from "@react-email/components"
|
||||
|
||||
type ResetPasswordEmailProps = {
|
||||
resetPasswordUrl: string
|
||||
storeName: string
|
||||
}
|
||||
|
||||
function Template({ resetPasswordUrl, storeName }: ResetPasswordEmailProps) {
|
||||
return (
|
||||
<Tailwind>
|
||||
<Html className="font-sans bg-gray-100">
|
||||
<Head />
|
||||
<Preview>Reset your password</Preview>
|
||||
<Body className="bg-white my-10 mx-auto w-full max-w-2xl">
|
||||
{/* Main Content */}
|
||||
<Container className="p-6 text-center">
|
||||
<Heading className="text-lg font-semibold text-black mb-6">
|
||||
You have submitted a password change request.
|
||||
</Heading>
|
||||
|
||||
<Section className="text-center my-8">
|
||||
<Button
|
||||
href={resetPasswordUrl}
|
||||
className="bg-black text-white py-3 px-8 inline-block"
|
||||
>
|
||||
Reset password
|
||||
</Button>
|
||||
</Section>
|
||||
|
||||
<Text className="text-sm text-black leading-relaxed mt-6">
|
||||
If you didn't request a password reset, you can safely ignore this
|
||||
email.
|
||||
</Text>
|
||||
</Container>
|
||||
|
||||
{/* Footer */}
|
||||
<Section className="bg-gray-50 p-6 mt-10">
|
||||
<Text className="text-center text-black text-xs mt-4">
|
||||
© {new Date().getFullYear()} {storeName}, Inc. All rights reserved.
|
||||
</Text>
|
||||
</Section>
|
||||
</Body>
|
||||
</Html>
|
||||
</Tailwind>
|
||||
)
|
||||
}
|
||||
|
||||
export default function getResetPasswordTemplate(props?: ResetPasswordEmailProps) {
|
||||
return (
|
||||
<Template
|
||||
resetPasswordUrl={props?.resetPasswordUrl ?? "#"}
|
||||
storeName={props?.storeName ?? "Demo Store"}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```ts title="src/subscribers/reset-password.ts"
|
||||
import {
|
||||
SubscriberArgs,
|
||||
type SubscriberConfig,
|
||||
} from "@medusajs/medusa"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
import { render, pretty } from "@react-email/render"
|
||||
import getResetPasswordTemplate from "../emails/reset-password"
|
||||
|
||||
export default async function resetPasswordTokenHandler({
|
||||
event: { data: {
|
||||
entity_id: email,
|
||||
token,
|
||||
actor_type,
|
||||
} },
|
||||
container,
|
||||
}: SubscriberArgs<{ entity_id: string, token: string, actor_type: string }>) {
|
||||
const notificationModuleService = container.resolve(
|
||||
Modules.NOTIFICATION
|
||||
)
|
||||
const config = container.resolve("configModule")
|
||||
const query = container.resolve("query")
|
||||
|
||||
const {
|
||||
data: [store],
|
||||
} = await query.graph({
|
||||
entity: "store",
|
||||
fields: ["name"],
|
||||
})
|
||||
|
||||
let urlPrefix = ""
|
||||
|
||||
if (actor_type === "customer") {
|
||||
urlPrefix = config.admin.storefrontUrl || "https://storefront.com"
|
||||
} else {
|
||||
const backendUrl = config.admin.backendUrl !== "/" ? config.admin.backendUrl :
|
||||
"http://localhost:9000"
|
||||
const adminPath = config.admin.path
|
||||
urlPrefix = `${backendUrl}${adminPath}`
|
||||
}
|
||||
|
||||
const resetPasswordUrl = `${urlPrefix}/reset-password?token=${token}&email=${email}`
|
||||
|
||||
const html = await pretty(
|
||||
await render(getResetPasswordTemplate({
|
||||
resetPasswordUrl,
|
||||
storeName: store.name,
|
||||
}))
|
||||
)
|
||||
|
||||
await notificationModuleService.createNotifications({
|
||||
to: email,
|
||||
// optional: set from email
|
||||
// from: "no-reply@yourdomain.com"
|
||||
channel: "email",
|
||||
content: {
|
||||
html,
|
||||
subject: `Reset your password - ${store.name}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const config: SubscriberConfig = {
|
||||
event: "auth.password_reset",
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
@@ -38,6 +38,9 @@ The following environment variable names are reserved by Cloud and you can't use
|
||||
- `AWS_ACCESS_KEY_ID`
|
||||
- `AWS_SECRET_ACCESS_KEY`
|
||||
- `AWS_REGION`
|
||||
- `MEDUSA_CLOUD_API_KEY`
|
||||
- `MEDUSA_CLOUD_ENVIRONMENT_HANDLE`
|
||||
- `MEDUSA_CLOUD_EMAILS_ENDPOINT`
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ Refer to the [Sign Up for Cloud](./sign-up/page.mdx) guide to get started.
|
||||
- **Instant Preview Environments**: Isolated preview environments are created on every pull request in your repository.
|
||||
- **Managed Resources**: Medusa manages your project's resources, including a Postgres database, a Redis server, and an S3 bucket.
|
||||
- **Medusa Cache**: Benefit from performance improvements of business-critical APIs by reducing heavy operations to the database. [Medusa Cache](./cache/page.mdx) is available out of the box at no additional cost.
|
||||
- **Medusa Emails**: Send transactional emails such as order confirmations and password resets using [Medusa Emails](./emails/react-email/page.mdx) with zero configuration. Available for all plans.
|
||||
- **Zero Downtime Deployments**: Medusa rolls out changes to production with zero downtime, never interrupting your users.
|
||||
- **Autoscaling**: Environments scale to meet traffic demands.
|
||||
- **Logging**: Monitor your project's runtime and build logs, so you can easily debug issues in your project.
|
||||
|
||||
@@ -13,7 +13,7 @@ export const generatedEditDates = {
|
||||
"app/logs/page.mdx": "2025-10-15T15:24:53.277Z",
|
||||
"app/update-medusa/page.mdx": "2025-06-25T07:57:30.170Z",
|
||||
"app/connect-storefront/page.mdx": "2025-06-25T07:47:00.499Z",
|
||||
"app/environments/environment-variables/page.mdx": "2025-10-15T15:26:47.364Z",
|
||||
"app/environments/environment-variables/page.mdx": "2025-11-12T15:42:19.187Z",
|
||||
"app/environments/long-lived/page.mdx": "2025-10-15T15:27:08.743Z",
|
||||
"app/environments/preview/page.mdx": "2025-11-03T12:50:29.841Z",
|
||||
"app/faq/page.mdx": "2025-10-08T14:43:21.930Z",
|
||||
@@ -24,6 +24,8 @@ export const generatedEditDates = {
|
||||
"app/sign-up/page.mdx": "2025-11-04T06:51:07.747Z",
|
||||
"app/comparison/page.mdx": "2025-10-22T14:44:01.898Z",
|
||||
"app/billing/plans/page.mdx": "2025-10-08T14:49:27.009Z",
|
||||
"app/cache/page.mdx": "2025-10-21T14:27:54.005Z",
|
||||
"app/deployments/troubleshooting/page.mdx": "2025-10-17T14:44:22.894Z"
|
||||
"app/cache/page.mdx": "2025-11-12T14:37:24.809Z",
|
||||
"app/deployments/troubleshooting/page.mdx": "2025-10-17T14:44:22.894Z",
|
||||
"app/emails/page.mdx": "2025-11-12T15:41:36.777Z",
|
||||
"app/emails/react-email/page.mdx": "2025-11-12T15:41:56.365Z"
|
||||
}
|
||||
@@ -157,6 +157,23 @@ export const generatedSidebars = [
|
||||
"title": "Cache",
|
||||
"path": "/cache",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"type": "link",
|
||||
"title": "Emails",
|
||||
"path": "/emails",
|
||||
"children": [
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"type": "link",
|
||||
"title": "React Email Templates",
|
||||
"path": "/emails/react-email",
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -107,6 +107,18 @@ export const sidebar = [
|
||||
title: "Cache",
|
||||
path: "/cache",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
title: "Emails",
|
||||
path: "/emails",
|
||||
children: [
|
||||
{
|
||||
type: "link",
|
||||
title: "React Email Templates",
|
||||
path: "/emails/react-email",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user