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-contact-us

v0.0.26

Published

Manage storefront email subscriptions (opt-ins and opt-outs) in Medusa Admin.

Readme

A comprehensive Medusa v2 plugin for managing contact requests and email subscriptions. The plugin provides a complete solution: database-backed contact requests with configurable status workflows, email subscriptions, admin APIs, helper utilities, and a polished admin UI.

Features:

  • 📝 Contact request management with status workflow and email notifications
  • ✉️ Email subscription management (opt-ins and opt-outs)
  • 🔄 Configurable status transitions and validation
  • 📧 Automatic email notifications on status changes
  • 🎯 Payload validation against configurable field definitions

Features

Email Subscriptions

  • ✉️ Storefront opt-in helper + admin list for email subscriptions (subscribed/unsubscribed)
  • 🖥️ React Admin extension with list view, filters, and search
  • 🔌 Frontend helper (upsertContactSubscription) to abstract API wiring
  • 🧪 Table-driven tests for helper logic

Contact Requests

  • 📝 Contact request management with configurable status workflow
  • 🔄 Status transition validation with configurable allowed transitions
  • 📧 Email notifications on status changes (configurable per transition)
  • 🎯 Payload validation against configurable field definitions
  • 📊 Admin API for listing, filtering, and managing requests
  • 🔌 Storefront helper (submitContactRequest) for easy integration
  • 📈 Status history tracking with timestamps and actor information

Installation

yarn add medusa-contact-us
# or
npm install medusa-contact-us

Configuration

Inside medusa-config.ts register the plugin:

import type { ConfigModule } from "@medusajs/framework/types"
import {
  ContactSubscriptionModule,
  ContactRequestModule,
} from "medusa-contact-us"

const plugins = [
  {
    resolve: "medusa-contact-us",
    options: {
      // Contact Request Configuration
      default_status: "pending",
      payload_fields: [
        {
          key: "subject",
          type: "text",
          required: true,
          label: "Subject",
          placeholder: "Enter subject",
        },
        {
          key: "message",
          type: "textarea",
          required: true,
          label: "Message",
          placeholder: "Enter your message",
        },
        {
          key: "priority",
          type: "select",
          required: false,
          label: "Priority",
          options: [
            { value: "low", label: "Low" },
            { value: "medium", label: "Medium" },
            { value: "high", label: "High" },
          ],
        },
      ],
      allowed_statuses: ["pending", "in_progress", "resolved", "closed"],
      status_transitions: [
        {
          from: null,
          to: "pending",
          send_email: false,
        },
        {
          from: "pending",
          to: "in_progress",
          send_email: true,
          email_subject: "Your request is being processed",
          email_template: null, // Optional: path to email template
        },
        {
          from: "in_progress",
          to: "resolved",
          send_email: true,
          email_subject: "Your request has been resolved",
        },
        {
          from: "resolved",
          to: "closed",
          send_email: false,
        },
      ],
      email: {
        enabled: true,
        default_subject: "Contact Request Status Update",
        default_template: null, // Optional: default email template path
      },
    },
  },
]

// Register the modules
const modules = [ContactSubscriptionModule, ContactRequestModule]

export default {
  projectConfig: {
    // Your Medusa project configuration
    // database_url: process.env.DATABASE_URL,
    // ...
  },
  plugins,
  modules,
} satisfies ConfigModule

Configuration Options

Contact Request Options

  • default_status (string, optional): Default status for new requests. Default: "pending"
  • payload_fields (array, optional): Field definitions for payload validation. Each field supports:
    • key (string, required): Field identifier
    • type (string, required): Field type - "text", "textarea", "number", "email", "select", "checkbox"
    • required (boolean, optional): Whether field is required
    • label (string, optional): Display label
    • placeholder (string, optional): Placeholder text
    • options (array, optional): For select type - [{ value: string, label: string }]
    • validation (object, optional): Validation rules (min, max, pattern)
  • allowed_statuses (array, optional): List of allowed status values. Default: ["pending", "in_progress", "resolved", "closed"]
  • status_transitions (array, optional): Allowed status transitions. Each transition supports:
    • from (string | null): Source status (null = initial status)
    • to (string, required): Target status
    • send_email (boolean, optional): Whether to send email on this transition
    • email_subject (string, optional): Custom email subject for this transition
    • email_template (string | null, optional): Custom email template path
  • email (object, optional): Email notification settings
    • enabled (boolean, optional): Enable/disable email notifications. Default: true
    • default_subject (string, optional): Default email subject. Default: "Contact Request Status Update"
    • default_template (string | null, optional): Default email template path

REST API

Storefront (open)

Contact Requests

Create a contact request:

curl -X POST https://your-medusa.com/store/contact-requests \
  -H "Content-Type: application/json" \
  -H "x-publishable-api-key: pk_storefront" \
  -d '{
        "email": "[email protected]",
        "payload": {
          "subject": "Order inquiry",
          "message": "I need help with my order #12345"
        },
        "metadata": {
          "source_page": "contact"
        },
        "source": "storefront"
      }'

Body fields:

  • email – required, valid email address
  • payload – optional, JSON object with custom fields (validated against configured payload_fields)
  • metadata – optional, additional metadata
  • source – optional, request source identifier (default: "storefront")

Response:

{
  "request": {
    "id": "creq_123",
    "email": "[email protected]",
    "payload": {
      "subject": "Order inquiry",
      "message": "I need help with my order #12345"
    },
    "status": "pending",
    "status_history": [
      {
        "from": null,
        "to": "pending",
        "changed_at": "2024-11-29T16:33:17.000Z"
      }
    ],
    "metadata": {
      "source_page": "contact"
    },
    "source": "storefront",
    "created_at": "2024-11-29T16:33:17.000Z",
    "updated_at": "2024-11-29T16:33:17.000Z"
  }
}

Email subscriptions

curl -X POST https://your-medusa.com/store/contact-email-subscriptions \
  -H "Content-Type: application/json" \
  -H "x-publishable-api-key: pk_storefront" \
  -d '{
        "email": "[email protected]",
        "status": "subscribed",
        "source": "footer"
      }'

Body fields:

  • email – required, unique per entry (case-insensitive).
  • status – optional, defaults to subscribed. Pass "unsubscribed" to honor opt-outs.
  • metadata / source – optional context stored alongside the entry.

Response:

{
  "subscription": {
    "id": "csub_123",
    "email": "[email protected]",
    "status": "subscribed",
    "source": "footer",
    "created_at": "2024-11-26T10:00:00.000Z"
  }
}

Admin (requires admin auth cookie/token)

Contact Requests

List contact requests:

curl -X GET "https://your-medusa.com/admin/contact-requests?status=pending&[email protected]&limit=20&offset=0" \
  -H "Authorization: Bearer <token>"

Query parameters:

  • email – optional, filter by email (partial match)
  • status – optional, filter by status
  • source – optional, filter by source
  • created_at.gte – optional, filter by creation date (ISO 8601)
  • created_at.lte – optional, filter by creation date (ISO 8601)
  • limit – optional, number of results (default: 20, max: 100)
  • offset – optional, pagination offset (default: 0)
  • order – optional, sort field: created_at, updated_at, email (default: created_at)
  • order_direction – optional, sort direction: ASC or DESC (default: DESC)

Get contact request details:

curl -X GET "https://your-medusa.com/admin/contact-requests/creq_123" \
  -H "Authorization: Bearer <token>"

Response includes the request and next_allowed_statuses array for the admin UI:

{
  "request": {
    "id": "creq_123",
    "email": "[email protected]",
    "payload": { ... },
    "status": "pending",
    "status_history": [ ... ],
    "metadata": { ... },
    "source": "storefront",
    "created_at": "2024-11-29T16:33:17.000Z",
    "updated_at": "2024-11-29T16:33:17.000Z"
  },
  "next_allowed_statuses": ["in_progress"]
}

Create contact request (admin):

curl -X POST https://your-medusa.com/admin/contact-requests \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token>" \
  -d '{
        "email": "[email protected]",
        "payload": {
          "subject": "Support request",
          "message": "Need assistance"
        },
        "source": "admin"
      }'

Update contact request status:

curl -X POST https://your-medusa.com/admin/contact-requests/creq_123/status \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token>" \
  -d '{
        "status": "in_progress"
      }'

Note: Only status transitions defined in status_transitions configuration are allowed. The API will return an error if an invalid transition is attempted.

Email Subscriptions

List subscriptions:

curl -X GET "https://your-medusa.com/admin/contact-email-subscriptions?status=subscribed&limit=20" \
  -H "Authorization: Bearer <token>"

Query parameters:

  • status – optional, filter by subscribed or unsubscribed
  • q – optional, search by email
  • limit – optional, number of results (default: 20)
  • offset – optional, pagination offset

Admin UI

After running medusa admin dev --plugins medusa-contact-us, the sidebar will include Contact email list:

  • List view – search by email, filter by status (subscribed/unsubscribed), inspect creation dates and unsubscribe timestamps.
  • All UI components follow the Medusa UI kit spacing (8pt grid), color, and accessibility guidelines.

Frontend helper

Skip hand-writing fetch calls by importing the provided helpers. Storefront requests must include a publishable API key (create one under Settings → API Keys in the Medusa admin). The helpers automatically attach the header for you.

Contact Requests

Submit a contact request from your storefront:

import { submitContactRequest } from "medusa-contact-us/helpers"

const result = await submitContactRequest(
  {
    email: "[email protected]",
    payload: {
      subject: "Order inquiry",
      message: "I need help with my order #12345",
      priority: "high",
    },
    metadata: {
      source_page: "contact",
      user_agent: navigator.userAgent,
    },
    source: "storefront",
  },
  {
    baseUrl: "https://store.myshop.com",
    publishableApiKey: "pk_test_storefront",
  }
)

console.log("Request created:", result.request.id)

Using a Medusa JS client:

import Medusa from "@medusajs/medusa-js"
import { submitContactRequest } from "medusa-contact-us/helpers"

const medusa = new Medusa({
  baseUrl: "https://store.myshop.com",
  publishableKey: "pk_live_client",
})

await submitContactRequest(
  {
    email: "[email protected]",
    payload: {
      subject: "Support request",
      message: "Need assistance",
    },
  },
  {
    client: medusa,
    publishableApiKey: "pk_live_client",
  }
)

For SSR or edge runtimes, preconfigure the helper:

import { createSubmitContactRequest } from "medusa-contact-us/helpers"

export const submitRequest = createSubmitContactRequest({
  baseUrl: process.env.NEXT_PUBLIC_MEDUSA_URL,
  publishableApiKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
})

export async function action(formData: FormData) {
  await submitRequest({
    email: formData.get("email") as string,
    payload: {
      subject: formData.get("subject") as string,
      message: formData.get("message") as string,
    },
    source: "contact_form",
  })
}

Email subscriptions

import { upsertContactSubscription } from "medusa-contact-us/helpers"

await upsertContactSubscription(
  {
    email: "[email protected]",
    status: "subscribed",
    metadata: { channel: "footer" },
  },
  {
    baseUrl: "https://store.myshop.com",
    publishableApiKey: "pk_test_storefront",
  }
)

Using a Medusa JS client keeps credentials in one place while still letting you override headers (including publishable keys) per call:

import Medusa from "@medusajs/medusa-js"
import { upsertContactSubscription } from "medusa-contact-us/helpers"

const medusa = new Medusa({
  baseUrl: "https://store.myshop.com",
  publishableKey: "pk_live_client",
})

await upsertContactSubscription(
  {
    email: "[email protected]",
    status: "subscribed",
  },
  {
    client: medusa,
    publishableApiKey: "pk_live_client",
    headers: {
      Cookie: "connect.sid=...",
    },
  }
)

For SSR or edge runtimes, preconfigure the helper once:

import { createUpsertContactSubscription } from "medusa-contact-us/helpers"

export const upsertSubscription = createUpsertContactSubscription({
  baseUrl: process.env.NEXT_PUBLIC_MEDUSA_URL,
  publishableApiKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
})

export async function action(formData: FormData) {
  await upsertSubscription({
    email: formData.get("email") as string,
    status: formData.get("unsubscribe") ? "unsubscribed" : "subscribed",
    source: "footer_form",
  })
}

Shared helper options

  • publishableApiKey – Automatically sets the x-publishable-api-key header when no Medusa client is used.
  • baseUrl – Storefront API origin (ignored when client is provided).
  • client – Pre-configured Medusa JS/SDK instance (reuses its base URL and publishable key).
  • fetchImpl – Custom fetch implementation (SSR, React Native, etc.).
  • headers – Additional headers merged into the request (e.g., session cookie, localization). Values you pass here override the defaults, including the publishable key header if you need a per-request key.

Customer-authenticated endpoints still require the appropriate session cookie or JWT. Provide those via the headers option if they're not already managed by the browser fetch call.

Status Workflow

Contact requests follow a configurable status workflow:

  1. Initial Status: New requests are created with the default_status (typically "pending")
  2. Status Transitions: Only transitions defined in status_transitions are allowed
  3. Status History: All status changes are tracked with timestamps and actor information
  4. Email Notifications: Emails can be sent on specific transitions when send_email: true

Example Workflow

pending → in_progress → resolved → closed

In this example:

  • Admin can move requests from pending to in_progress (email sent)
  • Admin can move requests from in_progress to resolved (email sent)
  • Admin can move requests from resolved to closed (no email)

Admin UI Integration

When fetching a contact request via the admin API, the response includes next_allowed_statuses:

{
  "request": { ... },
  "next_allowed_statuses": ["in_progress"]
}

Use this array to populate a status dropdown in your admin UI, ensuring only valid transitions are shown.

Database Migrations

After installing the plugin, run migrations to create the required tables:

npx medusa db:migrate

This will create:

  • contact_email_subscription table (for email subscriptions)
  • contact_request table (for contact requests)

Note: If you're developing the plugin locally, generate migrations after model changes:

npx medusa plugin:db:generate

Testing & build

yarn test   # runs Vitest table-driven suites
yarn build  # compiles the plugin via `medusa plugin:build`

Always run yarn build after development to ensure the bundler succeeds before publishing or yalc linking.

Troubleshooting

  • Admin UI blank: Rebuild the plugin (yarn build) and restart the Admin app with the plugin registered in medusa-config.ts.
  • Status transition errors: Ensure the transition is defined in status_transitions configuration. Only transitions from the current status to an allowed next status are permitted.
  • Payload validation errors: Check that all required fields defined in payload_fields are provided and match the expected types.
  • Email notifications not sending: Verify that email.enabled is true in configuration and that the transition has send_email: true. Ensure the notification service is properly configured in your Medusa instance.
  • Service resolution errors: Make sure both ContactSubscriptionModule and ContactRequestModule are registered in the modules array in medusa-config.ts.