medusa-notification-provider
v0.0.2
Published
A starter for Medusa plugins.
Downloads
189
Maintainers
Readme
Table of Contents
- Compatibility
- Overview
- Installation
- Initialization Guide
- Configuration
- Usage Examples
- Use Cases
- Payload Structure
- Error Handling
- Troubleshooting
- Best Practices
Compatibility
This plugin is compatible with versions >= 2.4.0 of @medusajs/medusa.
Overview
medusa-notification-provider provides two powerful notification providers for Medusa v2:
🔔 FCM Provider
Send push notifications to:
- Android devices via Firebase Cloud Messaging
- iOS devices via Apple Push Notification Service (APNs)
- Web browsers via Web Push API
Perfect for: Order updates, shipping notifications, promotional alerts, real-time updates
💬 WhatsApp Provider
Send text messages via WhatsApp Cloud API to:
- Customer phone numbers
- Support teams
- Admin notifications
Perfect for: Order confirmations, shipping updates, customer support, marketing campaigns
Installation
Step 1: Install the Package
npm install medusa-notification-provider firebase-admin
# or
yarn add medusa-notification-provider firebase-adminStep 2: Verify Installation
Ensure the package is installed correctly:
npm list medusa-notification-providerInitialization Guide
FCM Provider Setup
Step 1: Create a Firebase Project
- Go to Firebase Console
- Click "Add project" or select an existing project
- Follow the setup wizard to create your project
- Note your Project ID (you'll need this later)
Step 2: Enable Cloud Messaging API
- In Firebase Console, go to Project Settings (gear icon)
- Navigate to the Cloud Messaging tab
- Ensure Cloud Messaging API (Legacy) is enabled
- If not enabled, click "Enable"
Step 3: Create a Service Account
- In Firebase Console, go to Project Settings → Service Accounts
- Click "Generate New Private Key"
- A JSON file will be downloaded - keep this secure!
- Open the JSON file and extract the following values:
project_id→ This is yourFCM_PROJECT_IDclient_email→ This is yourFCM_CLIENT_EMAILprivate_key→ This is yourFCM_PRIVATE_KEY
Step 4: Register FCM Provider
Add to your medusa-config.ts:
import { defineConfig } from "@medusajs/framework/utils"
module.exports = defineConfig({
projectConfig: {
// ... your existing config
},
modules: [
{
resolve: "@medusajs/medusa/notification",
options: {
providers: [
{
resolve: "medusa-notification-provider/providers/fcm",
id: "fcm",
options: {
channels: ["push"],
// Required FCM configuration
projectId: process.env.FCM_PROJECT_ID,
clientEmail: process.env.FCM_CLIENT_EMAIL,
privateKey: process.env.FCM_PRIVATE_KEY,
},
},
],
},
},
],
})Required Provider Options:
projectId- Your Firebase project ID (required)clientEmail- Firebase service account email (required)privateKey- Firebase service account private key (required)
Important Notes:
- Keep the
\ncharacters inFCM_PRIVATE_KEY- they are required - Wrap the private key in double quotes in your
.envfile - Never commit your
.envfile to version control - All FCM options are required
WhatsApp Provider Setup
Step 1: Create a Meta Developer Account
- Go to Meta for Developers
- Sign in with your Facebook account
- Click "My Apps" → "Create App"
- Select "Business" as the app type
- Fill in app details and create the app
Step 2: Set Up WhatsApp Business Account
- In your Meta App, go to "WhatsApp" → "Getting Started"
- Click "Set up WhatsApp Business Account"
- Follow the setup wizard:
- Accept Terms of Service
- Choose a display name
- Verify your business (if required)
Step 3: Get Your Credentials
- Go to "WhatsApp" → "API Setup"
- You'll find:
- Temporary Access Token (for testing)
- Phone Number ID (format:
123456789012345) - WhatsApp Business Account ID (WABA ID)
For Production:
- Create a System User in Meta Business Suite
- Generate a Permanent Access Token
- Assign WhatsApp permissions to the System User
Step 4: Register WhatsApp Provider
Add to your medusa-config.ts:
import { defineConfig } from "@medusajs/framework/utils"
module.exports = defineConfig({
modules: [
{
resolve: "@medusajs/medusa/notification",
options: {
providers: [
{
resolve: "medusa-notification-provider/providers/whatsapp",
id: "whatsapp",
options: {
channels: ["sms"],
// Required WhatsApp configuration
accessToken: process.env.WHATSAPP_ACCESS_TOKEN,
wabaId: process.env.WHATSAPP_WABA_ID,
phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID,
// Optional configuration
enabled: process.env.WHATSAPP_ENABLED !== "false", // Default: true
graphAPIVersion: process.env.WHATSAPP_GRAPH_API_VERSION || "v21.0",
},
},
],
},
},
],
})Required Provider Options:
accessToken- WhatsApp Cloud API access token (required)wabaId- WhatsApp Business Account ID (required)phoneNumberId- Phone Number ID from your WhatsApp Business account (required)
Optional Provider Options:
enabled- Enable WhatsApp provider (default:true)graphAPIVersion- WhatsApp Graph API version (default:"v21.0")
Complete Configuration Example
Here's a complete medusa-config.ts with both providers:
import { defineConfig } from "@medusajs/framework/utils"
module.exports = defineConfig({
projectConfig: {
databaseUrl: process.env.DATABASE_URL,
// ... other config
},
modules: [
{
resolve: "@medusajs/medusa/notification",
options: {
providers: [
// FCM Provider for Push Notifications
{
resolve: "medusa-notification-provider/providers/fcm",
id: "fcm",
options: {
channels: ["push"],
},
},
// WhatsApp Provider for SMS/WhatsApp Messages
{
resolve: "medusa-notification-provider/providers/whatsapp",
id: "whatsapp",
options: {
channels: ["sms"],
},
},
],
},
},
],
})Usage Examples
Basic Usage
Send FCM Push Notification
import { Modules } from "@medusajs/framework/utils"
// In your service or workflow
const notificationService = container.resolve(Modules.NOTIFICATION) as {
create?: (payload: unknown) => Promise<unknown>
}
const payload = {
to: "fcm-device-token-here", // FCM registration token
channel: "push",
data: {
title: "Order Shipped!",
body: "Your order #1234 has been shipped and is on its way.",
order_id: "1234",
action: "view_order",
},
}
await notificationService.create?.(payload)Send WhatsApp Message
const payload = {
to: "+1234567890", // Phone number with country code
channel: "sms",
data: {
title: "Order Confirmed",
body: "Your order #1234 has been confirmed. Thank you for your purchase!",
},
}
await notificationService.create?.(payload)Workflow Steps
Example 1: Send Notification After Order Creation
// src/workflows/steps/send-order-notification-step.ts
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { Modules, MedusaError } from "@medusajs/framework/utils"
type SendOrderNotificationInput = {
orderId: string
customerId: string
deviceToken?: string
phoneNumber?: string
}
export const sendOrderNotificationStep = createStep(
"send-order-notification",
async (input: SendOrderNotificationInput, { container }) => {
const notificationService = container.resolve(Modules.NOTIFICATION) as {
create?: (payload: unknown) => Promise<unknown>
createNotifications?: (payloads: unknown[]) => Promise<unknown>
}
const payloads = []
// Prepare FCM notification if device token available
if (input.deviceToken) {
payloads.push({
to: input.deviceToken,
channel: "push",
data: {
title: "Order Confirmed!",
body: `Your order #${input.orderId} has been confirmed.`,
order_id: input.orderId,
action: "view_order",
},
})
}
// Prepare WhatsApp notification if phone number available
if (input.phoneNumber) {
payloads.push({
to: input.phoneNumber,
channel: "sms",
data: {
title: "Order Confirmed",
body: `Your order #${input.orderId} has been confirmed. Thank you!`,
},
})
}
if (payloads.length === 0) {
return new StepResponse({ sent: false, reason: "No recipients" })
}
try {
if (typeof notificationService.createNotifications === "function") {
await notificationService.createNotifications(payloads)
} else if (typeof notificationService.create === "function") {
// Send individually if createNotifications not available
for (const payload of payloads) {
await notificationService.create(payload)
}
} else {
throw new MedusaError(
MedusaError.Types.UNEXPECTED_STATE,
"Notification service not available"
)
}
return new StepResponse({ sent: true, count: payloads.length })
} catch (error) {
throw new MedusaError(
MedusaError.Types.UNEXPECTED_STATE,
`Failed to send notifications: ${error instanceof Error ? error.message : String(error)}`
)
}
}
)Example 2: Send Shipping Notification
// src/workflows/steps/send-shipping-notification-step.ts
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"
type SendShippingNotificationInput = {
orderId: string
trackingNumber: string
deviceToken: string
phoneNumber?: string
}
export const sendShippingNotificationStep = createStep(
"send-shipping-notification",
async (input: SendShippingNotificationInput, { container }) => {
const notificationService = container.resolve(Modules.NOTIFICATION) as {
createNotifications?: (payloads: unknown[]) => Promise<unknown>
}
const payloads = [
{
to: input.deviceToken,
channel: "push",
data: {
title: "Order Shipped!",
body: `Your order #${input.orderId} has been shipped. Track it with: ${input.trackingNumber}`,
order_id: input.orderId,
tracking_number: input.trackingNumber,
action: "track_order",
},
},
]
// Add WhatsApp notification if phone number provided
if (input.phoneNumber) {
payloads.push({
to: input.phoneNumber,
channel: "sms",
data: {
title: "Order Shipped",
body: `Your order #${input.orderId} has been shipped!\nTracking: ${input.trackingNumber}`,
},
})
}
await notificationService.createNotifications?.(payloads)
return new StepResponse({ sent: true })
}
)Subscribers
Example: Send Notification on Order Created
// src/subscribers/order-created.ts
import { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"
import { OrderCreatedEventData } from "@medusajs/framework/types"
export default async function orderCreatedHandler({
event: { data },
container,
}: SubscriberArgs<OrderCreatedEventData>) {
const notificationService = container.resolve(Modules.NOTIFICATION) as {
createNotifications?: (payloads: unknown[]) => Promise<unknown>
}
// Get customer information (you'll need to fetch this)
const customerService = container.resolve(Modules.CUSTOMER) as {
retrieveCustomer: (id: string) => Promise<{ phone?: string | null }>
}
const customer = await customerService.retrieveCustomer(data.customer_id)
const phoneNumber = customer?.phone
const payloads = []
// Add FCM notification if device token available
// Note: You'll need to store device tokens separately
const deviceToken = await getDeviceTokenForCustomer(data.customer_id)
if (deviceToken) {
payloads.push({
to: deviceToken,
channel: "push",
data: {
title: "Order Confirmed!",
body: `Your order #${data.id} has been confirmed.`,
order_id: data.id,
action: "view_order",
},
})
}
// Add WhatsApp notification if phone number available
if (phoneNumber && phoneNumber.startsWith("+")) {
payloads.push({
to: phoneNumber,
channel: "sms",
data: {
title: "Order Confirmed",
body: `Your order #${data.id} has been confirmed. Thank you!`,
},
})
}
if (payloads.length > 0) {
try {
await notificationService.createNotifications?.(payloads)
} catch (error) {
// Log error but don't throw - don't block order creation
container.resolve("logger")?.error("Failed to send order notification", {
error: error instanceof Error ? error.message : String(error),
orderId: data.id,
})
}
}
}
export const config: SubscriberConfig = {
event: "order.created",
}
// Helper function to get device token (implement based on your token storage)
async function getDeviceTokenForCustomer(customerId: string): Promise<string | null> {
// Implement your logic to retrieve device token
// This might involve querying a database or token management service
return null
}API Routes
Example: Send Notification via API Endpoint
// src/api/store/notifications/send/route.ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { Modules, MedusaError } from "@medusajs/framework/utils"
export async function POST(req: MedusaRequest, res: MedusaResponse) {
// Get authenticated customer
const authContext = (req as { auth_context?: { actor_id?: string; actor_type?: string } })
.auth_context
const customerId = authContext?.actor_id
if (!customerId || authContext?.actor_type !== "customer") {
throw new MedusaError(
MedusaError.Types.UNAUTHORIZED,
"Customer authentication required"
)
}
const { deviceToken, phoneNumber, title, body, data } = req.body as {
deviceToken?: string
phoneNumber?: string
title: string
body: string
data?: Record<string, unknown>
}
if (!title || !body) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Title and body are required"
)
}
if (!deviceToken && !phoneNumber) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Either deviceToken or phoneNumber is required"
)
}
const notificationService = req.scope.resolve(Modules.NOTIFICATION) as {
createNotifications?: (payloads: unknown[]) => Promise<unknown>
}
const payloads = []
if (deviceToken) {
payloads.push({
to: deviceToken,
channel: "push",
data: {
title,
body,
...data,
},
})
}
if (phoneNumber) {
payloads.push({
to: phoneNumber,
channel: "sms",
data: {
title,
body,
},
})
}
try {
await notificationService.createNotifications?.(payloads)
res.json({
success: true,
message: "Notifications sent successfully",
count: payloads.length,
})
} catch (error) {
throw new MedusaError(
MedusaError.Types.UNEXPECTED_STATE,
`Failed to send notifications: ${error instanceof Error ? error.message : String(error)}`
)
}
}Use Cases
1. E-commerce Order Notifications
Scenario: Notify customers about order status changes
// When order is placed
await sendNotification({
deviceToken: customer.deviceToken,
phoneNumber: customer.phone,
title: "Order Placed",
body: `Your order #${orderId} has been placed successfully!`,
})
// When order is shipped
await sendNotification({
deviceToken: customer.deviceToken,
phoneNumber: customer.phone,
title: "Order Shipped",
body: `Your order #${orderId} is on the way! Tracking: ${trackingNumber}`,
})
// When order is delivered
await sendNotification({
deviceToken: customer.deviceToken,
phoneNumber: customer.phone,
title: "Order Delivered",
body: `Your order #${orderId} has been delivered. Enjoy!`,
})2. Promotional Campaigns
Scenario: Send promotional messages to customers
// Send promotional push notification
await sendNotification({
deviceToken: customer.deviceToken,
title: "Special Offer!",
body: "Get 20% off on all products. Use code SAVE20",
data: {
promo_code: "SAVE20",
discount: "20%",
action: "shop_now",
},
})3. Customer Support
Scenario: Notify support team about new tickets
// Notify support team via WhatsApp
await sendNotification({
phoneNumber: "+1234567890", // Support team phone
channel: "sms",
title: "New Support Ticket",
body: `New ticket #${ticketId} from ${customerName}`,
})4. Inventory Alerts
Scenario: Notify admins about low stock
// Notify admin via WhatsApp
await sendNotification({
phoneNumber: adminPhoneNumber,
channel: "sms",
title: "Low Stock Alert",
body: `Product ${productName} is running low. Current stock: ${stock}`,
})5. Payment Reminders
Scenario: Remind customers about pending payments
await sendNotification({
deviceToken: customer.deviceToken,
phoneNumber: customer.phone,
title: "Payment Reminder",
body: `Please complete payment for order #${orderId}. Amount: $${amount}`,
})Payload Structure
FCM Provider Payload
{
to: string, // Required: FCM device token
channel: "push", // Required: Must be "push"
data: {
title: string, // Optional: Notification title
body: string, // Optional: Notification body
// Custom data fields (converted to strings)
order_id?: string,
action?: string,
// Platform-specific options
android?: {
priority: "high" | "normal",
notification: {
sound: string,
channelId: string,
},
},
apns?: {
payload: {
aps: {
sound: string,
badge: number,
},
},
},
webpush?: {
notification: {
icon: string,
badge: string,
},
},
}
}WhatsApp Provider Payload
{
to: string, // Required: Phone number with country code (e.g., "+1234567890")
channel: "sms", // Required: Must be "sms"
data: {
title: string, // Optional: Message title (combined with body)
body: string, // Optional: Message body
}
}Message Format: WhatsApp messages are formatted as:
{title}
{body}If only title or body is provided, only that field is sent.
Error Handling
Best Practices
Always wrap notification sending in try-catch blocks:
try {
await notificationService.create(payload)
} catch (error) {
// Log error for debugging
logger.error("Failed to send notification", {
error: error instanceof Error ? error.message : String(error),
payload: { to: payload.to.substring(0, 5) + "...", channel: payload.channel },
})
// Don't throw in non-critical scenarios (e.g., order creation)
// Only throw if notification is critical to the workflow
}Common Errors
FCM Errors
- Invalid token: Token is expired or invalid
- Solution: Remove token from database and request new token from client
- Missing configuration: Firebase credentials not set
- Solution: Check environment variables are set correctly
WhatsApp Errors
- Invalid phone number: Phone number format is incorrect
- Solution: Ensure phone number starts with
+and includes country code
- Solution: Ensure phone number starts with
- Rate limit exceeded: Too many messages sent
- Solution: Implement rate limiting and retry logic
- Unauthorized: Access token is invalid or expired
- Solution: Regenerate access token in Meta Business Suite
Troubleshooting
FCM Provider Issues
Issue: "Firebase Admin SDK configuration missing"
Solution:
- Check environment variables are set in
.env - Verify
FCM_PRIVATE_KEYincludes\ncharacters - Ensure private key is wrapped in double quotes
- Restart your Medusa server after changing environment variables
Issue: "Invalid device token"
Solution:
- Verify token is valid FCM registration token
- Check token hasn't expired
- Ensure token matches the correct Firebase project
WhatsApp Provider Issues
Issue: "Phone number must include country code"
Solution:
- Ensure phone numbers start with
+(e.g.,+1234567890) - Include country code (e.g.,
+1for US,+91for India)
Issue: "WhatsApp is not configured or enabled"
Solution:
- Set
WHATSAPP_ENABLED=truein.env - Verify all WhatsApp environment variables are set
- Check access token is valid and not expired
Issue: "Failed to send WhatsApp message" with 401 error
Solution:
- Regenerate access token in Meta Business Suite
- Ensure System User has WhatsApp permissions
- Verify phone number ID is correct
General Issues
Issue: Provider not found
Solution:
- Verify provider is registered in
medusa-config.ts - Check provider path is correct:
medusa-notification-provider/providers/{provider-name} - Ensure plugin is installed:
npm list medusa-notification-provider - Rebuild plugin:
npm run build
Issue: Notifications not sending
Solution:
- Check server logs for error messages
- Verify notification service is resolved correctly
- Ensure channel matches provider configuration (
pushfor FCM,smsfor WhatsApp) - Test with minimal payload first
Best Practices
1. Store Device Tokens Securely
// Create a module or service to manage device tokens
// Store tokens in database with customer association
// Implement token refresh logic2. Implement Retry Logic
async function sendWithRetry(
payload: unknown,
maxRetries = 3
): Promise<void> {
for (let i = 0; i < maxRetries; i++) {
try {
await notificationService.create(payload)
return
} catch (error) {
if (i === maxRetries - 1) throw error
await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1)))
}
}
}3. Batch Notifications
// Send multiple notifications in one call
const payloads = [
{ to: token1, channel: "push", data: {...} },
{ to: token2, channel: "push", data: {...} },
{ to: phone1, channel: "sms", data: {...} },
]
await notificationService.createNotifications?.(payloads)4. Validate Before Sending
function validateNotificationPayload(payload: {
to: string
channel: string
data?: Record<string, unknown>
}): boolean {
if (!payload.to) return false
if (payload.channel === "push" && !payload.to.startsWith("fcm")) return false
if (payload.channel === "sms" && !payload.to.startsWith("+")) return false
return true
}5. Logging and Monitoring
// Log all notification attempts
logger.info("Sending notification", {
channel: payload.channel,
recipient: payload.to.substring(0, 5) + "...",
timestamp: new Date().toISOString(),
})
// Monitor success/failure rates
// Set up alerts for high failure ratesGetting Started
Visit the Quickstart Guide to set up a server.
Visit the Plugins documentation to learn more about plugins and how to create them.
What is Medusa
Medusa is a set of commerce modules and tools that allow you to build rich, reliable, and performant commerce applications without reinventing core commerce logic. The modules can be customized and used to build advanced ecommerce stores, marketplaces, or any product that needs foundational commerce primitives. All modules are open-source and freely available on npm.
Learn more about Medusa's architecture and commerce modules in the Docs.
Community & Contributions
The community and core team are available in GitHub Discussions, where you can ask for support, discuss roadmap, and share ideas.
Join our Discord server to meet other community members.
