Events
Medusa’s event system enables loose coupling between modules through asynchronous event-driven communication. The Event Bus distributes events to subscribers, allowing you to react to domain events without tight dependencies.
Event Bus
The Event Bus is responsible for:
- Publishing events - Emit events from services and workflows
- Routing events - Deliver events to registered subscribers
- Queuing - Handle event delivery asynchronously
- Grouping - Batch events within transactions
Medusa provides two Event Bus implementations: event-bus-local for development and event-bus-redis for production with distributed workers.
Event Bus Service
The Event Bus service manages event distribution:
import type {
Event,
EventBusTypes,
InternalModuleDeclaration,
Logger,
MedusaContainer,
Message,
Subscriber,
} from "@medusajs/framework/types"
import { AbstractEventBusModuleService } from "@medusajs/framework/utils"
import { EventEmitter } from "events"
export default class LocalEventBusService extends AbstractEventBusModuleService {
protected readonly logger_: Logger
protected readonly eventEmitter_: EventEmitter
protected groupedEventsMap_: Map<string, Message[]>
constructor(
{ logger }: MedusaContainer & InjectedDependencies,
moduleOptions = {},
moduleDeclaration: InternalModuleDeclaration
) {
super(...arguments)
this.logger_ = logger ?? console
this.eventEmitter_ = new EventEmitter()
this.eventEmitter_.setMaxListeners(Infinity)
this.groupedEventsMap_ = new Map()
}
async emit<T = unknown>(
eventsData: Message<T> | Message<T>[],
options: Record<string, unknown> = {}
): Promise<void> {
const normalizedEventsData = Array.isArray(eventsData)
? eventsData
: [eventsData]
for (const eventData of normalizedEventsData) {
await this.groupOrEmitEvent({
...eventData,
options,
})
}
}
}
Source: packages/modules/event-bus-local/src/services/event-bus-local.ts:24-79
Emitting Events
From Services with @EmitEvents
The @EmitEvents decorator automatically emits events after service methods:
import {
InjectManager,
InjectTransactionManager,
MedusaContext,
EmitEvents,
} from "@medusajs/framework/utils"
import type { Context } from "@medusajs/framework/types"
export class ApiKeyModuleService extends MedusaService {
@InjectManager()
@EmitEvents() // Automatically emits apiKey.created
async createApiKeys(
data: ApiKeyTypes.CreateApiKeyDTO | ApiKeyTypes.CreateApiKeyDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<ApiKeyTypes.ApiKeyDTO | ApiKeyTypes.ApiKeyDTO[]> {
const [createdApiKeys, generatedTokens] = await this.createApiKeys_(
Array.isArray(data) ? data : [data],
sharedContext
)
return Array.isArray(data) ? createdApiKeys : createdApiKeys[0]
}
}
Source: packages/modules/api-key/src/services/api-key-module-service.ts:123-133
Events are automatically named: {entityName}.{operation}
apiKey.created
product.updated
order.deleted
From Workflows with createHook
Workflows emit events using hooks:
import {
createWorkflow,
WorkflowData,
WorkflowResponse,
createHook,
} from "@medusajs/framework/workflows-sdk"
import { createApiKeysStep } from "../steps"
export const createApiKeysWorkflow = createWorkflow(
"create-api-keys",
(input: WorkflowData<CreateApiKeysWorkflowInput>) => {
const apiKeys = createApiKeysStep(input)
const apiKeysCreated = createHook("apiKeysCreated", {
apiKeys,
})
return new WorkflowResponse(apiKeys, {
hooks: [apiKeysCreated],
})
}
)
Source: packages/core/core-flows/src/api-key/workflows/create-api-keys.ts:51-64
Manual Event Emission
Emit events manually using the event bus:
import { Modules } from "@medusajs/framework/utils"
import type { IEventBusModuleService } from "@medusajs/framework/types"
export const POST = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const eventBus = req.scope.resolve<IEventBusModuleService>(
Modules.EVENT_BUS
)
await eventBus.emit({
name: "custom.event",
data: {
id: "123",
status: "completed",
},
})
res.json({ success: true })
}
Emit Multiple Events
Emit multiple events at once:
import type { IEventBusModuleService } from "@medusajs/framework/types"
await eventBus.emit([
{
name: "order.placed",
data: { id: orderId },
},
{
name: "inventory.reserved",
data: { items: orderItems },
},
{
name: "payment.authorized",
data: { payment_id: paymentId },
},
])
Event Subscribers
Subscribers listen for events and execute logic in response.
Creating a Subscriber
Create a subscriber in src/subscribers/:
import type {
SubscriberArgs,
SubscriberConfig,
} from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"
export default async function handleProductCreated({
event: { data },
container,
}: SubscriberArgs<{ id: string }>) {
const productModule = container.resolve(Modules.PRODUCT)
const product = await productModule.retrieveProduct(data.id)
console.log(`Product created: ${product.title}`)
// Send notification, update search index, etc.
}
export const config: SubscriberConfig = {
event: "product.created",
}
Subscriber Configuration
The config export defines subscriber metadata:
import type { SubscriberConfig } from "@medusajs/framework"
export const config: SubscriberConfig = {
event: "product.created", // Event name to listen for
}
Multiple Events
Subscribe to multiple events:
export default async function handleProductEvents({
event: { name, data },
container,
}: SubscriberArgs<{ id: string }>) {
const productModule = container.resolve(Modules.PRODUCT)
const product = await productModule.retrieveProduct(data.id)
switch (name) {
case "product.created":
console.log(`Product created: ${product.title}`)
break
case "product.updated":
console.log(`Product updated: ${product.title}`)
break
case "product.deleted":
console.log(`Product deleted: ${data.id}`)
break
}
}
export const config: SubscriberConfig = {
event: ["product.created", "product.updated", "product.deleted"],
}
Wildcard Subscribers
Subscribe to all events:
export default async function handleAllEvents({
event: { name, data },
container,
}: SubscriberArgs<any>) {
console.log(`Event: ${name}`, data)
}
export const config: SubscriberConfig = {
event: "*", // Listen to all events
}
Event Data Structure
Events follow a consistent structure:
type Message<T = unknown> = {
name: string // Event name (e.g., "product.created")
data: T // Event payload
metadata?: {
eventGroupId?: string // For grouped events
[key: string]: any // Custom metadata
}
options?: {
internal?: boolean // Internal event (no logging)
delay?: number // Delay in milliseconds
[key: string]: any
}
}
Service Event Data
Service events include entity IDs:
{
name: "product.created",
data: {
id: "prod_123" // Single entity
}
}
// Or for batch operations:
{
name: "product.deleted",
data: {
ids: ["prod_123", "prod_456"] // Multiple entities
}
}
Workflow Hook Data
Workflow hooks include custom data:
{
name: "apiKeysCreated",
data: {
apiKeys: [
{ id: "apikey_123", title: "Storefront" },
{ id: "apikey_456", title: "Mobile App" },
]
}
}
Event Grouping
Group events within transactions to emit them together:
import { Modules } from "@medusajs/framework/utils"
import type { IEventBusModuleService } from "@medusajs/framework/types"
export const myWorkflow = createWorkflow(
"my-workflow",
(input: WorkflowInput) => {
const eventGroupId = "transaction-123"
// All events with the same eventGroupId are grouped
await eventBus.emit({
name: "order.placed",
data: { id: orderId },
metadata: { eventGroupId },
})
await eventBus.emit({
name: "inventory.reserved",
data: { items: orderItems },
metadata: { eventGroupId },
})
// Events are released when explicitly triggered
await eventBus.releaseGroupedEvents(eventGroupId)
}
)
Event grouping is useful for workflows where you want to emit multiple events only after all steps succeed.
Delayed Events
Emit events with a delay:
import type { IEventBusModuleService } from "@medusajs/framework/types"
await eventBus.emit({
name: "reminder.send",
data: { userId: "user_123" },
options: {
delay: 60000, // Delay 60 seconds
},
})
Common Event Patterns
Audit Logging
import type { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"
export default async function auditLogger({
event: { name, data },
container,
}: SubscriberArgs<any>) {
const logger = container.resolve("logger")
logger.info(`[AUDIT] ${name}`, {
event: name,
data,
timestamp: new Date().toISOString(),
})
}
export const config: SubscriberConfig = {
event: "*",
}
Send Notifications
import type { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"
export default async function sendOrderConfirmation({
event: { data },
container,
}: SubscriberArgs<{ id: string }>) {
const orderModule = container.resolve(Modules.ORDER)
const notificationModule = container.resolve(Modules.NOTIFICATION)
const order = await orderModule.retrieveOrder(data.id, {
relations: ["customer"],
})
await notificationModule.createNotifications({
to: order.customer.email,
channel: "email",
template: "order-confirmation",
data: { order },
})
}
export const config: SubscriberConfig = {
event: "order.placed",
}
Update Search Index
import type { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"
export default async function updateProductIndex({
event: { name, data },
container,
}: SubscriberArgs<{ id: string }>) {
const productModule = container.resolve(Modules.PRODUCT)
const indexModule = container.resolve(Modules.INDEX)
if (name === "product.deleted") {
await indexModule.delete("products", data.id)
return
}
const product = await productModule.retrieveProduct(data.id)
await indexModule.upsert("products", {
id: product.id,
title: product.title,
description: product.description,
handle: product.handle,
})
}
export const config: SubscriberConfig = {
event: ["product.created", "product.updated", "product.deleted"],
}
Sync to External System
import type { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"
export default async function syncToErp({
event: { data },
container,
}: SubscriberArgs<{ id: string }>) {
const orderModule = container.resolve(Modules.ORDER)
const order = await orderModule.retrieveOrder(data.id)
// Sync to external ERP system
await fetch("https://erp.example.com/api/orders", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
external_id: order.id,
total: order.total,
items: order.items,
}),
})
}
export const config: SubscriberConfig = {
event: "order.placed",
}
Event Bus Providers
Local Event Bus
For development and single-server deployments:
import { Modules } from "@medusajs/framework/utils"
export default defineConfig({
modules: [
{
resolve: "@medusajs/event-bus-local",
options: {
// Local event bus options
},
},
],
})
Redis Event Bus
For production with distributed workers:
import { Modules } from "@medusajs/framework/utils"
export default defineConfig({
modules: [
{
resolve: "@medusajs/event-bus-redis",
options: {
redisUrl: process.env.REDIS_URL,
},
},
],
})
Always use Redis Event Bus in production environments with multiple workers to ensure events are distributed correctly.
Best Practices
Keep Subscribers Idempotent
Subscribers may be called multiple times for the same event. Design them to handle duplicate calls gracefully.
Handle Failures
Wrap subscriber logic in try-catch blocks to prevent one subscriber from blocking others.
Avoid Long-Running Operations
Subscribers should be fast. Offload heavy work to background jobs or async workflows.
Use Typed Event Data
Define TypeScript types for event payloads to catch errors at compile time.
Subscribers run asynchronously and don’t block the main execution flow. They’re perfect for side effects like notifications, logging, and syncing.
Next Steps
Workflows
Emit events from workflow hooks
Services
Emit events automatically with @EmitEvents