medusa-contact-us
v0.0.29
Published
Manage storefront email subscriptions (opt-ins and opt-outs) in Medusa Admin.
Maintainers
Readme
medusa-contact-us
Medusa v2 plugin for contact request workflows and storefront email subscription management with admin tooling.
Plugin Overview
medusa-contact-us provides two core capabilities:
- Contact requests: create, list, assign, comment, and status-manage customer support/contact entries.
- Contact email subscriptions: capture and update storefront subscription/unsubscription state.
It includes:
- Two Medusa modules (
contact_requests,contact_email_subscriptions) - Store and admin API routes
- Workflows/steps for request creation and status transitions
- Optional notification dispatch on configured status transitions
- Admin route extensions for managing requests and subscriptions
- Storefront helper utilities for easier frontend integration
This solves the need for a structured contact/support intake workflow and consent list management directly in Medusa.
Medusa Version
- Built for Medusa v2 (
@medusajs/framework/@medusajs/medusa2.11.2in package).
Installation & Setup
1) Install
npm install medusa-contact-usyarn add medusa-contact-us2) Register plugin and modules in medusa-config.ts
import { defineConfig } from "@medusajs/framework/utils"
import {
ContactSubscriptionModule,
ContactRequestModule,
} from "medusa-contact-us"
export default defineConfig({
modules: [
ContactSubscriptionModule,
ContactRequestModule,
],
plugins: [
{
resolve: "medusa-contact-us",
options: {
default_status: "pending",
payload_fields: [],
allowed_statuses: ["pending", "in_progress", "resolved", "closed"],
status_transitions: [
{ from: null, to: "pending", send_email: false },
{ from: "pending", to: "in_progress", send_email: true },
{ from: "in_progress", to: "resolved", send_email: true },
{ from: "resolved", to: "closed", send_email: false },
],
email: {
enabled: true,
default_subject: "Contact Request Status Update",
default_template: null,
},
},
},
],
})3) Run database migrations
npx medusa db:migrateConfiguration (config.ts / plugin options)
Options are resolved in modules/contact-requests/utils/resolve-options.ts.
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
| default_status | string | No | "pending" | Initial status for new contact requests. |
| payload_fields | PayloadFieldConfig[] | No | [] | Dynamic payload validation schema for custom request fields. |
| allowed_statuses | string[] | No | ["pending","in_progress","resolved","closed"] | Allowed status values. |
| status_transitions | StatusTransitionConfig[] | No | built-in transition chain | Defines valid status changes and optional email behavior per transition. |
| email.enabled | boolean | No | true | Enables notification send attempts in status workflow. |
| email.default_subject | string | No | "Contact Request Status Update" | Fallback email subject. |
| email.default_template | string \| null | No | null | Fallback template identifier/path if provided. |
PayloadFieldConfig shape
| Field | Type | Required | Notes |
|---|---|---|---|
| key | string | Yes | Payload field key. |
| type | "text" \| "textarea" \| "number" \| "email" \| "select" \| "checkbox" | Yes | Runtime validator uses this type. |
| required | boolean | No | Requires field presence in payload. |
| label | string | No | UI/display metadata. |
| placeholder | string | No | UI/display metadata. |
| options | { value: string; label: string }[] | No | Used by select type validation. |
| validation | { min?: number; max?: number; pattern?: string } | No | Numeric/string rules used during payload validation. |
StatusTransitionConfig shape
| Field | Type | Required | Notes |
|---|---|---|---|
| from | string \| null | Yes | null means initial transition. |
| to | string | Yes | Next status. |
| send_email | boolean | No | If true and email enabled, workflow attempts notification. |
| email_template | string | No | Transition-specific template override. |
| email_subject | string | No | Transition-specific subject override. |
Environment Variables
The plugin source directly references one env variable:
| Variable | Required | Purpose | Example |
|---|---|---|---|
| NODE_ENV | No | Adds stack traces to certain API error responses in development mode. | development |
⚠️ Note: Frontend/helper examples may use environment variables in the host app, but plugin runtime logic only directly reads
NODE_ENV.
REST APIs / Routes
Store routes
1) POST /store/contact-email-subscriptions
- Auth: Public (
AUTHENTICATE = false) - Body:
| Field | Type | Required |
|---|---|---|
| email | string (email) | Yes |
| status | "subscribed" \| "unsubscribed" | No |
| metadata | Record<string, unknown> | No |
| source | string | No |
- Response:
{ subscription }
2) GET /store/contact-requests
- Auth: Customer required (
AUTHENTICATE = true, actor_type must becustomer) - Query:
| Field | Type | Required | Default |
|---|---|---|---|
| limit | number | No | 10 |
| offset | number | No | 0 |
| order | "created_at" \| "updated_at" | No | "created_at" |
| order_direction | "ASC" \| "DESC" | No | "DESC" |
- Behavior: returns only requests matching authenticated customer email; includes comments per request.
- Response:
{ requests, count, offset, limit }
3) POST /store/contact-requests
- Auth: Customer required (
AUTHENTICATE = true) - Body:
| Field | Type | Required |
|---|---|---|
| email | string (email) | Yes |
| payload | Record<string, unknown> | No |
| metadata | Record<string, unknown> | No |
| source | string | No |
- Behavior: runs
create-contact-requestworkflow. - Response:
{ request }
4) GET /store/contact-requests/:id
- Auth: Customer required (
AUTHENTICATE = true) - Behavior: ensures request belongs to authenticated customer email.
- Response:
{ request, comments }
Admin routes
5) GET /admin/contact-email-subscriptions
- Auth: Admin route
- Query:
| Field | Type | Required | Default |
|---|---|---|---|
| status | "subscribed" \| "unsubscribed" | No | - |
| q | string | No | - |
| limit | number | No | 50 |
| offset | number | No | 0 |
- Response:
{ subscriptions, count, offset, limit }
6) GET /admin/contact-requests
- Auth: Admin route
- Query:
| Field | Type | Required |
|---|---|---|
| email | string | No |
| status | string | No |
| source | string | No |
| created_at.gte | datetime string | No |
| created_at.lte | datetime string | No |
| limit | number (1..100) | No |
| offset | number (>=0) | No |
| order | "created_at" \| "updated_at" \| "email" | No |
| order_direction | "ASC" \| "DESC" | No |
- Response:
{ requests, count, offset, limit }
7) POST /admin/contact-requests
- Auth: Admin route
- Body: same shape as store create request
- Behavior: source defaults to
"admin"; runs create workflow. - Response:
{ request }
8) GET /admin/contact-requests/:id
- Auth: Admin route
- Response:
{ request, comments, next_allowed_statuses }
9) POST /admin/contact-requests/:id/status
- Auth: Admin route
- Body:
{ status: string } - Behavior: runs update status workflow + transition resolution + optional notification.
- Response:
{ request }
10) POST /admin/contact-requests/:id/assign
- Auth: Admin route
- Body:
{ assign_to: string | null } - Response:
{ request }
11) GET /admin/contact-requests/:id/comments
- Auth: Admin route
- Response:
{ comments }
12) POST /admin/contact-requests/:id/comments
- Auth: Admin route
- Body:
| Field | Type | Required |
|---|---|---|
| comment | string | No |
| images | string[] | No |
- Response:
{ comment }(HTTP201)
Health routes
13) GET /admin/plugin
- Returns HTTP
200.
14) GET /store/plugin
- Returns HTTP
200.
Important cURL examples
curl -X POST "http://localhost:9000/store/contact-email-subscriptions" \
-H "Content-Type: application/json" \
-H "x-publishable-api-key: pk_test_xxx" \
-d '{"email":"[email protected]","status":"subscribed","source":"footer"}'curl -X POST "http://localhost:9000/admin/contact-requests" \
-H "Authorization: Bearer <ADMIN_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","payload":{"subject":"Help","message":"Need support"}}'curl -X POST "http://localhost:9000/admin/contact-requests/creq_123/status" \
-H "Authorization: Bearer <ADMIN_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"status":"in_progress"}'Services
1) ContactSubscriptionModuleService
Location: src/modules/contact-subscriptions/service.ts
Manages email subscription records.
Key methods:
| Method | Description |
|---|---|
| upsertSubscription(input) | Creates/updates subscription by normalized email; handles unsubscribed_at. |
| listWithFilters(selector, config) | Filtered/paginated list wrapper. |
| normalizeSubscription(raw) | Normalizes raw data to DTO shape. |
2) ContactRequestModuleService
Location: src/modules/contact-requests/service.ts
Manages request lifecycle, payload validation, status transitions, assignment, and comments.
Key methods:
| Method | Description |
|---|---|
| createRequest(input) | Validates email/payload fields, sets default status and status history. |
| updateStatus(input) | Enforces transition rules and updates status history. |
| listRequests(selector, config) | Filtered/paginated listing for admin/store use. |
| getRequest(id) | Retrieves single request DTO. |
| assignRequest(input) | Assigns/unassigns request owner/admin reference. |
| createComment(input) / listComments(contactRequestId) | Manages request comments with image URLs. |
| getOptions() / getNextAllowedStatuses() / getStatusTransition() | Exposes resolved workflow/status config behavior. |
Workflows & Steps (Medusa v2)
Workflows
| Workflow | Input | Output | Purpose |
|---|---|---|---|
| create-contact-request | CreateContactRequestInput | { request } | Creates new contact request through step abstraction. |
| update-contact-request-status | UpdateContactRequestStatusInput | { request } | Updates status, resolves transition, optionally sends notification. |
Steps
| Step | Purpose |
|---|---|
| create-contact-request | Calls module service createRequest. |
| update-contact-request-status | Updates status and captures previous status. |
| resolve-status-transition | Loads transition config + resolved plugin options. |
| send-status-notification | Sends notification via Modules.NOTIFICATION when enabled/configured. |
Subscribers / Event Hooks
No subscribers/event handlers are implemented.
Admin UI / Widgets
Implemented as admin route extensions:
| Route | Location in Admin | Renders | Interactions / Data |
|---|---|---|---|
| Contact email list | Sidebar (Envelope icon) | Subscription table with filters/search/load-more | Calls /admin/contact-email-subscriptions; status/search filtering. |
| Contact Requests | Sidebar (ChatBubbleLeftRight icon) | Requests table with status/source/email filters | Calls /admin/contact-requests; navigates to detail view. |
| Contact Request Details | Detail page route | Full request view: status updates, assignment dropdown (admin users), comment management with image upload | Uses /admin/contact-requests/:id, status/assign/comments endpoints, /admin/users, and /admin/uploads. |
No defineWidgetConfig zone widgets are implemented.
Models & Entities
contact_email_subscription
Fields:
| Field | Type | Nullable |
|---|---|---|
| id | id | No |
| email | text | No |
| status | text | No |
| metadata | json | Yes |
| source | text | Yes |
| unsubscribed_at | datetime | Yes |
Migration includes timestamps (created_at, updated_at, deleted_at) and indexes on email (unique), status, deleted_at.
contact_request
Fields:
| Field | Type | Nullable |
|---|---|---|
| id | id | No |
| email | text | No |
| payload | json | Yes |
| status | text | No |
| status_history | json | Yes |
| metadata | json | Yes |
| source | text | Yes |
| assign_to | text | Yes |
Migration adds indexes on email, status, source, created_at, deleted_at, and assign_to.
contact_request_comment
Fields:
| Field | Type | Nullable |
|---|---|---|
| id | id | No |
| admin_id | text | No |
| contact_request_id | text | No |
| comment | text | Yes |
| images | json | Yes |
Migration adds timestamps and indexes on contact_request_id, admin_id, deleted_at.
Relationships to core Medusa entities are not explicitly modeled via ORM relations; linking is via IDs/fields (for example assign_to, admin_id).
Use Cases & Examples
Storefront contact form ingestion
- Submit via
POST /store/contact-requests, then manage in admin queue.
- Submit via
Support team status workflow
- Move requests through configured transitions (
pending -> in_progress -> resolved) using admin status endpoint.
- Move requests through configured transitions (
Status update notifications
- Configure transitions with
send_email: trueand let workflow trigger notification service dispatch.
- Configure transitions with
Agent assignment and internal notes
- Assign requests to admins and add comments/images on detail page.
Newsletter consent tracking
- Use
POST /store/contact-email-subscriptionshelper-backed flow to maintain subscribe/unsubscribe state.
- Use
Troubleshooting
Invalid status transition errors
- Cause: attempted transition not defined in
status_transitions. - Fix: update plugin options or use
next_allowed_statusesfrom detail endpoint to drive allowed UI actions.
Payload validation errors on request creation
- Cause: payload fields/types do not match configured
payload_fields. - Fix: align payload keys/types and required fields with plugin config.
Notification send failures
- Cause:
Modules.NOTIFICATIONnot configured or incompatible interface (create/createNotificationsmissing). - Fix: configure Medusa notification module/provider and verify transition/email settings.
Comments endpoint returns empty with warning
- Cause: comment table migration not applied.
- Fix: run
npx medusa db:migrateto createcontact_request_comment.
Unauthorized store request access
- Cause: store contact request routes require authenticated customer (
AUTHENTICATE = trueand actor type check). - Fix: call with valid customer auth/session.
