{"_id":"@aumiqx/medusa-plugin-messages","name":"@aumiqx/medusa-plugin-messages","dist-tags":{"alpha":"0.1.0-alpha.0","latest":"0.1.0-alpha.0"},"versions":{"0.1.0-alpha.0":{"name":"@aumiqx/medusa-plugin-messages","version":"0.1.0-alpha.0","description":"Self-hosted messaging for Medusa v2 marketplaces — customer↔vendor and vendor↔admin threads with Server-Sent Events real-time push, unread tracking, and zero external SaaS.","author":{"name":"Aumiqx Technologies"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/aumiqx/medusa-plugin-messages.git"},"homepage":"https://github.com/aumiqx/medusa-plugin-messages#readme","bugs":{"url":"https://github.com/aumiqx/medusa-plugin-messages/issues"},"keywords":["medusa","medusajs","medusa-plugin","messaging","chat","sse","marketplace","b2c"],"exports":{"./package.json":"./package.json","./workflows":"./.medusa/server/src/workflows/index.js","./.medusa/server/src/modules/*":"./.medusa/server/src/modules/*/index.js","./modules/*":"./.medusa/server/src/modules/*/index.js","./providers/*":"./.medusa/server/src/providers/*/index.js","./admin":{"import":"./.medusa/server/src/admin/index.mjs","require":"./.medusa/server/src/admin/index.js","default":"./.medusa/server/src/admin/index.js"},"./*":"./.medusa/server/src/*.js"},"scripts":{"build":"medusa plugin:build","dev":"medusa plugin:develop","prepublishOnly":"medusa plugin:build","typecheck":"tsc --noEmit"},"devDependencies":{"@medusajs/admin-sdk":"2.11.3","@medusajs/cli":"2.11.3","@medusajs/framework":"2.11.3","@medusajs/medusa":"2.11.3","@medusajs/test-utils":"2.11.3","@medusajs/ui":"4.0.25","@medusajs/icons":"2.11.3","@swc/core":"^1.7.28","@types/node":"^20.0.0","@types/react":"^18.3.2","@types/react-dom":"^18.2.25","react":"^18.2.0","react-dom":"^18.2.0","ts-node":"^10.9.2","typescript":"^5.6.2","vite":"^5.4.21","zod":"^3.22.4"},"peerDependencies":{"@medusajs/admin-sdk":"^2.11.0","@medusajs/cli":"^2.11.0","@medusajs/framework":"^2.11.0","@medusajs/medusa":"^2.11.0","@medusajs/ui":"^4.0.0","@medusajs/icons":"^2.11.0","zod":"^3.22.0"},"engines":{"node":">=20"},"publishConfig":{"access":"public"},"gitHead":"f5fe7c4ba1b00212d6e30a5184d5aaaae8381cff","_id":"@aumiqx/medusa-plugin-messages@0.1.0-alpha.0","_nodeVersion":"24.14.0","_npmVersion":"11.9.0","dist":{"integrity":"sha512-Raa8fYSWmdEe/hJlujNiJIGW6K9SIq0/KTlnkAhYfXlszH3e9J0/xmITw9fIwZbzQHlnHvnR0qHDmrfM7Vt6ZA==","shasum":"ab556ad769155320c08802a94b67ce8c1ebe0d01","tarball":"https://registry.npmjs.org/@aumiqx/medusa-plugin-messages/-/medusa-plugin-messages-0.1.0-alpha.0.tgz","fileCount":73,"unpackedSize":144448,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCID6aX6AU5RiImu8a7M6pzSU7kCMEhb7TMaMEZ0lYWUMdAiEAqF1xy+yhdjpK9lhbBI54JwtxLZsb4k42iGEbvX8kt5s="}]},"_npmUser":{"name":"aumiqx","email":"axit@aumiqx.com"},"directories":{},"maintainers":[{"name":"aumiqx","email":"axit@aumiqx.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/medusa-plugin-messages_0.1.0-alpha.0_1776866082622_0.9871809848409363"},"_hasShrinkwrap":false}},"time":{"created":"2026-04-22T13:54:42.539Z","0.1.0-alpha.0":"2026-04-22T13:54:42.763Z","modified":"2026-04-22T13:54:42.950Z"},"maintainers":[{"name":"aumiqx","email":"axit@aumiqx.com"}],"description":"Self-hosted messaging for Medusa v2 marketplaces — customer↔vendor and vendor↔admin threads with Server-Sent Events real-time push, unread tracking, and zero external SaaS.","homepage":"https://github.com/aumiqx/medusa-plugin-messages#readme","keywords":["medusa","medusajs","medusa-plugin","messaging","chat","sse","marketplace","b2c"],"repository":{"type":"git","url":"git+https://github.com/aumiqx/medusa-plugin-messages.git"},"author":{"name":"Aumiqx Technologies"},"bugs":{"url":"https://github.com/aumiqx/medusa-plugin-messages/issues"},"license":"MIT","readme":"# @aumiqx/medusa-plugin-messages\n\nSelf-hosted messaging for **Medusa v2** marketplaces. Drop-in plugin that gives your store:\n\n- Customer ↔ vendor threads scoped to orders\n- Vendor ↔ admin support threads\n- Unread badge counts per participant\n- Server-Sent Events real-time push (with polling as a safety net)\n- Zero external SaaS dependency — messages live in your Postgres\n\nBuilt to replace TalkJS, Intercom widgets, and other per-MAU-priced chat tools for marketplace scenarios.\n\n## Why\n\nMost chat SaaS is priced per monthly active user. For a marketplace where *every buyer* is a potential chat user, that scales nastily. This plugin keeps everything in your own Postgres, on your own infra, under MIT license.\n\n## Requirements\n\n- Medusa v2 (`@medusajs/medusa >= 2.11`)\n- Node 20+\n- Postgres (comes with Medusa)\n\n## Install\n\n```bash\nnpm install @aumiqx/medusa-plugin-messages\n# or\nyarn add @aumiqx/medusa-plugin-messages\n```\n\nRegister in `medusa-config.ts`:\n\n```ts\nexport default defineConfig({\n  plugins: [\n    { resolve: \"@aumiqx/medusa-plugin-messages\", options: {} },\n  ],\n})\n```\n\nRun migrations:\n\n```bash\nnpx medusa db:migrate\n```\n\n## What's in the box\n\n### Data model\n\n```\nconversation\n  id, type (order_customer_vendor | vendor_admin),\n  order_id?, seller_id?, customer_id?,\n  subject?, last_message_at, metadata\n\nconversation_participant\n  id, actor_type (customer | seller | admin), actor_id,\n  last_read_at, last_seen_at\n\nmessage\n  id, author_actor_type, author_actor_id, body\n```\n\nEach participant tracks their own `last_read_at`, which is how unread counts work — messages created *after* a participant's `last_read_at`, by someone *other than them*.\n\n### API routes\n\nAll three scopes expose the same shape. Auth is enforced via the existing Medusa actor guards (admin bearer/cookie, customer bearer, etc.).\n\n| Scope | Method | Path | Purpose |\n|---|---|---|---|\n| Admin | `GET` | `/admin/conversations` | List (filters: `type`, `order_id`, `seller_id`, `customer_id`, `limit`, `offset`) |\n| Admin | `POST` | `/admin/conversations` | Open a vendor↔admin thread |\n| Admin | `GET` | `/admin/conversations/:id` | Thread + messages |\n| Admin | `POST` | `/admin/conversations/:id/messages` | Reply (auto-joins the admin as a participant) |\n| Admin | `POST` | `/admin/conversations/:id/read` | Mark read (auto-joins) |\n| Admin | `GET` | `/admin/conversations/:id/stream` | SSE push stream |\n| Store | `GET` | `/store/conversations` | Customer's threads |\n| Store | `POST` | `/store/conversations` | Open an order↔vendor thread (verifies order.customer_id matches caller) |\n| Store | `GET` | `/store/conversations/:id` | Thread + messages (participation check) |\n| Store | `POST` | `/store/conversations/:id/messages` | Send (participation check) |\n| Store | `POST` | `/store/conversations/:id/read` | Mark read |\n| Store | `GET` | `/store/conversations/:id/stream` | SSE push stream |\n\n### Workflows\n\nImport and call from your own code:\n\n```ts\nimport {\n  createOrGetConversationWorkflow,\n  sendMessageWorkflow,\n  markReadWorkflow,\n} from \"@aumiqx/medusa-plugin-messages/workflows\"\n```\n\n### Events\n\n```ts\nimport { MessageEvents } from \"@aumiqx/medusa-plugin-messages\"\n\nMessageEvents.CONVERSATION_CREATED  // 'messages.conversation.created'\nMessageEvents.MESSAGE_CREATED       // 'messages.message.created'\nMessageEvents.CONVERSATION_READ     // 'messages.conversation.read'\n```\n\nAll fired from workflows via `emitEventStep`, so they only fire after the workflow commits successfully. Wire your own subscribers (email, push, analytics, etc.) on these.\n\n## Extending\n\n### Vendor / seller routes\n\nThe plugin ships admin + store routes. If your stack is a marketplace with a dedicated seller auth scope (e.g. MercurJS), add a matching `/vendor/conversations/*` route set in your own project that resolves the current seller from `req.auth_context.actor_id` and forwards to the same workflows. Example stub:\n\n```ts\n// your-project/src/api/vendor/conversations/route.ts\nimport {\n  createOrGetConversationWorkflow,\n} from \"@aumiqx/medusa-plugin-messages/workflows\"\n// ... resolve sellerId from auth, then call the workflow ...\n```\n\n### Email fallback when participants are offline\n\nSubscribe to `messages.message.created`, check each non-author participant's `last_seen_at`, and fire your notification provider for anyone who's been idle beyond your threshold:\n\n```ts\nimport { SubscriberArgs, SubscriberConfig } from \"@medusajs/framework\"\nimport { Modules } from \"@medusajs/framework/utils\"\nimport {\n  MESSAGES_MODULE,\n  MessagesModuleService,\n  MessageEvents,\n} from \"@aumiqx/medusa-plugin-messages\"\n\nexport default async function handler({ event, container }: SubscriberArgs<{\n  message_id: string\n  conversation_id: string\n  author_actor_type: string\n  author_actor_id: string\n}>) {\n  const service = container.resolve<MessagesModuleService>(MESSAGES_MODULE)\n  const notifications = container.resolve(Modules.NOTIFICATION)\n\n  const participants = await service.listConversationParticipants({\n    conversation_id: event.data.conversation_id,\n  })\n\n  const OFFLINE_MS = 5 * 60 * 1000\n  const offline = participants.filter(\n    (p) =>\n      !(p.actor_type === event.data.author_actor_type &&\n        p.actor_id === event.data.author_actor_id) &&\n      (!p.last_seen_at ||\n        Date.now() - new Date(p.last_seen_at as any).getTime() > OFFLINE_MS)\n  )\n\n  // Resolve each participant's email from your own actor tables,\n  // then call notifications.createNotifications(...) with your template.\n}\n\nexport const config: SubscriberConfig = {\n  event: MessageEvents.MESSAGE_CREATED,\n  context: { subscriberId: \"messages-offline-email\" },\n}\n```\n\n### Storefront real-time from a Bearer-auth storefront\n\nBrowser `EventSource` can't send `Authorization` headers. If your storefront auth is Bearer-in-localStorage/cookie (not same-origin session), proxy the stream through a same-origin route handler that reads the token server-side and pipes the upstream response body. Next.js example:\n\n```ts\n// app/api/conversation-stream/[id]/route.ts\nexport const dynamic = \"force-dynamic\"\nexport const runtime = \"nodejs\"\n\nexport async function GET(req, { params }) {\n  const { id } = await params\n  const bearer = await getBearerFromCookie() // your helper\n  const upstream = await fetch(\n    `${process.env.MEDUSA_BACKEND_URL}/store/conversations/${id}/stream`,\n    {\n      headers: { Authorization: `Bearer ${bearer}`, Accept: \"text/event-stream\" },\n      signal: req.signal,\n    }\n  )\n  return new Response(upstream.body, {\n    headers: {\n      \"Content-Type\": \"text/event-stream\",\n      \"Cache-Control\": \"no-cache, no-transform\",\n      \"X-Accel-Buffering\": \"no\",\n    },\n  })\n}\n```\n\nThen point `new EventSource('/api/conversation-stream/<id>')` at this route.\n\n### Scaling beyond one process\n\nThe default `messageBus` is a single-process Node `EventEmitter`. It works on PM2 fork mode or a single docker container. If you scale horizontally, swap the bus for a Redis pub/sub adapter with the same `publish` / `subscribe` interface — the SSE route handler doesn't care about the implementation.\n\n## Frontend reference\n\nShape of a conversation (list endpoint response):\n\n```ts\ntype Conversation = {\n  id: string\n  type: \"order_customer_vendor\" | \"vendor_admin\"\n  order_id: string | null\n  seller_id: string | null\n  customer_id: string | null\n  subject: string | null\n  last_message_at: string | null\n  created_at: string\n  updated_at: string\n  unread_count: number\n  participants: Array<{\n    id: string\n    actor_type: \"customer\" | \"seller\" | \"admin\"\n    actor_id: string\n    last_read_at: string | null\n  }>\n}\n```\n\nSSE frames emit one event type you care about:\n\n```\nevent: message.created\ndata: {\"message_id\":\"msg_...\",\"author_actor_type\":\"seller\",\"author_actor_id\":\"...\"}\n```\n\nOn receiving that frame, refetch the thread (polling fallback does the same).\n\n## Status & limitations (v0.1.0-alpha.0)\n\n- Single-process `messageBus` — swap for Redis if horizontally scaling.\n- No attachments — bring your own file storage integration.\n- No typing indicators.\n- Hard cap of 500 messages per thread load — pagination pending.\n- SSE endpoints are participant-gated, not role-gated — any admin sees any thread they've been added to (admin routes auto-add).\n\n## License\n\nMIT © Aumiqx Technologies\n","readmeFilename":"README.md","_rev":"1-2ab190331e3c5abeaccae8743889ec56"}