npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

medusa-notification-provider

v0.0.2

Published

A starter for Medusa plugins.

Downloads

189

Readme

Table of Contents

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-admin

Step 2: Verify Installation

Ensure the package is installed correctly:

npm list medusa-notification-provider

Initialization Guide

FCM Provider Setup

Step 1: Create a Firebase Project

  1. Go to Firebase Console
  2. Click "Add project" or select an existing project
  3. Follow the setup wizard to create your project
  4. Note your Project ID (you'll need this later)

Step 2: Enable Cloud Messaging API

  1. In Firebase Console, go to Project Settings (gear icon)
  2. Navigate to the Cloud Messaging tab
  3. Ensure Cloud Messaging API (Legacy) is enabled
  4. If not enabled, click "Enable"

Step 3: Create a Service Account

  1. In Firebase Console, go to Project SettingsService Accounts
  2. Click "Generate New Private Key"
  3. A JSON file will be downloaded - keep this secure!
  4. Open the JSON file and extract the following values:
    • project_id → This is your FCM_PROJECT_ID
    • client_email → This is your FCM_CLIENT_EMAIL
    • private_key → This is your FCM_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 \n characters in FCM_PRIVATE_KEY - they are required
  • Wrap the private key in double quotes in your .env file
  • Never commit your .env file to version control
  • All FCM options are required

WhatsApp Provider Setup

Step 1: Create a Meta Developer Account

  1. Go to Meta for Developers
  2. Sign in with your Facebook account
  3. Click "My Apps""Create App"
  4. Select "Business" as the app type
  5. Fill in app details and create the app

Step 2: Set Up WhatsApp Business Account

  1. In your Meta App, go to "WhatsApp""Getting Started"
  2. Click "Set up WhatsApp Business Account"
  3. Follow the setup wizard:
    • Accept Terms of Service
    • Choose a display name
    • Verify your business (if required)

Step 3: Get Your Credentials

  1. Go to "WhatsApp""API Setup"
  2. 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
  • 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:

  1. Check environment variables are set in .env
  2. Verify FCM_PRIVATE_KEY includes \n characters
  3. Ensure private key is wrapped in double quotes
  4. Restart your Medusa server after changing environment variables

Issue: "Invalid device token"

Solution:

  1. Verify token is valid FCM registration token
  2. Check token hasn't expired
  3. 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., +1 for US, +91 for India)

Issue: "WhatsApp is not configured or enabled"

Solution:

  1. Set WHATSAPP_ENABLED=true in .env
  2. Verify all WhatsApp environment variables are set
  3. Check access token is valid and not expired

Issue: "Failed to send WhatsApp message" with 401 error

Solution:

  1. Regenerate access token in Meta Business Suite
  2. Ensure System User has WhatsApp permissions
  3. Verify phone number ID is correct

General Issues

Issue: Provider not found

Solution:

  1. Verify provider is registered in medusa-config.ts
  2. Check provider path is correct: medusa-notification-provider/providers/{provider-name}
  3. Ensure plugin is installed: npm list medusa-notification-provider
  4. Rebuild plugin: npm run build

Issue: Notifications not sending

Solution:

  1. Check server logs for error messages
  2. Verify notification service is resolved correctly
  3. Ensure channel matches provider configuration (push for FCM, sms for WhatsApp)
  4. 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 logic

2. 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 rates

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

Other channels