Packages

Core

The orchestration engine — createCommerce(), event bus, capability routing, and webhook dispatch.

The @commercejs/core package is the Commerce.js orchestration engine. It wires adapters, payment providers, notifications, analytics, events, and webhooks into a single createCommerce() entry point. It also provides orchestrator factories for multi-adapter composition.

Installation

pnpm add @commercejs/core @commercejs/types

Quick Start

import { createCommerce } from '@commercejs/core'
import { SallaAdapter } from '@commercejs/adapter-salla'
import { TapPaymentProvider } from '@commercejs/payment-tap'

const commerce = createCommerce({
  adapter: new SallaAdapter({ token: '...' }),
  payments: {
    tap: new TapPaymentProvider({ secretKey: '...' }),
  },
  defaultPayment: 'tap',
})

// Capability-checked catalog call
const products = await commerce.getProducts({ query: 'shirt' })

// Payment via registered provider
const session = await commerce.createPayment({
  amount: 99.99,
  currency: 'SAR',
})

Configuration

The CommerceConfig accepts these options:

OptionTypeRequiredDescription
adapterCommerceAdapterYesPlatform adapter instance (Salla, Medusa, etc.)
paymentsRecord<string, PaymentProvider>NoPayment providers keyed by ID
defaultPaymentstringNoDefault payment provider ID
webhooksWebhookEndpoint[]NoOutbound webhook endpoints
notificationsRecord<string, NotificationProvider>NoNotification providers keyed by ID
notificationRulesNotificationRule[]NoEvent → channel → provider dispatch rules
analyticsAnalyticsProvider[]NoAnalytics providers (all receive all events)
fetchtypeof fetchNoCustom fetch for webhooks (edge runtimes, testing)
sign(payload, secret) => stringNoHMAC signing for webhook payloads

Capability Routing

Every domain method checks the adapter's capabilities array before calling through. If the adapter doesn't support a domain, a CommerceError with code NOT_SUPPORTED (HTTP 501) is thrown.

// Check before calling
if (commerce.hasCapability('wishlist')) {
  const wishlist = await commerce.getWishlist()
}

// Or handle the error
try {
  await commerce.getWishlist()
} catch (err) {
  if (err.code === 'NOT_SUPPORTED') {
    console.log('This adapter does not support wishlists')
  }
}

Supported Domains

catalog · cart · checkout · orders · customers · wishlist · reviews · store · promotions · returns · brands · countries · locations · wholesale · auctions · rentals · gift-cards

Event Bus

Every state-changing operation emits a typed event. Subscribe to events for analytics, side effects, or plugin logic.

commerce.events.on('order.created', ({ order }) => {
  sendConfirmationEmail(order)
})

commerce.events.on('payment.confirmed', ({ session }) => {
  updateInventory(session)
})

// Wildcard — listen to everything
commerce.events.onAny((event, data) => {
  analytics.track(event, data)
})

// One-time listener
commerce.events.once('cart.created', ({ cart }) => {
  console.log('First cart created:', cart.id)
})

Event Catalog

EventPayload
product.viewed{ product }
cart.created{ cart }
cart.item.added{ cart, productId, quantity }
cart.item.updated{ cart, itemId, quantity }
cart.item.removed{ cart, itemId }
order.created{ order }
order.cancelled{ order }
payment.created{ session }
payment.confirmed{ session }
payment.failed{ session, error? }
payment.refunded{ session, amount }
customer.logged_in{ customer }
customer.registered{ customer }
customer.updated{ customer }
checkout.started{ cartId }
checkout.completed{ order, session }
return.created{ returnRequest }
return.cancelled{ returnRequest }

Multi-Provider Payments

Register multiple payment providers and select one per transaction:

const commerce = createCommerce({
  adapter,
  payments: {
    tap: new TapPaymentProvider({ secretKey: '...' }),
    stripe: new StripePaymentProvider({ secretKey: '...' }),
  },
  defaultPayment: 'tap',
})

// Use default provider
await commerce.createPayment({ amount: 50, currency: 'SAR' })

// Override per-call
await commerce.createPayment({ amount: 50, currency: 'USD' }, 'stripe')

// Confirm and refund
await commerce.confirmPayment('sess_123', 'tap')
await commerce.refundPayment({ sessionId: 'sess_123', amount: 25 }, 'tap')

Webhook Dispatcher

Register external endpoints to receive commerce events as HTTP POST requests with JSON payloads, retry logic, and optional HMAC signing.

const commerce = createCommerce({
  adapter,
  webhooks: [
    {
      id: 'crm',
      url: 'https://crm.example.com/webhook',
      events: ['order.created', 'customer.registered'],
      secret: 'whsec_...',
    },
    {
      id: 'analytics',
      url: 'https://analytics.example.com/hook',
      events: ['*'],  // Wildcard — all events
    },
  ],
  sign: async (payload, secret) => {
    const encoder = new TextEncoder()
    const key = await crypto.subtle.importKey(
      'raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'],
    )
    const sig = await crypto.subtle.sign('HMAC', key, encoder.encode(payload))
    return btoa(String.fromCharCode(...new Uint8Array(sig)))
  },
})

Webhook Delivery

Each webhook POST includes these headers:

HeaderDescription
Content-Typeapplication/json
X-Commerce-EventEvent name (e.g., order.created)
X-Commerce-DeliveryUnique delivery ID
X-Commerce-SignatureHMAC signature (if sign and secret are set)

Failed deliveries are retried with exponential backoff (1s → 2s → 4s). 4xx responses are not retried.

Cleanup

Call destroy() to remove all event listeners and webhook subscriptions:

commerce.destroy()

Orchestrator Factories

The core package exports three factory functions for composing domains from multiple adapter sources.

createOrchestrator()

Creates a CommerceOrchestrator from universal domains and an optional domain map:

import { createOrchestrator } from '@commercejs/core'

const orchestrator = createOrchestrator({
  name: 'my-store',
  catalog: myCatalogAdapter,
  store: myStoreAdapter,
  domains: {
    cart: myCartAdapter,
    orders: myOrderAdapter,
  },
})

orchestrator.supports('cart')     // true
orchestrator.supports('wishlist') // false
orchestrator.domain('cart')       // CartAdapter instance

createCompositeOrchestrator()

Composes domains from multiple adapter sources into a single orchestrator:

import { createCompositeOrchestrator } from '@commercejs/core'

const orchestrator = createCompositeOrchestrator({
  name: 'hybrid-store',
  providers: {
    catalog: shopifyAdapter,
    store: shopifyAdapter,
    cart: platformAdapter,
    customers: crmAdapter,
  },
})

withPlatformFallback()

Wraps an orchestrator with a fallback that fills missing domains:

import { withPlatformFallback } from '@commercejs/core'

// sallaAdapter supports catalog + store
// platformAdapter fills in cart, checkout, orders, customers...
const orchestrator = withPlatformFallback(sallaAdapter, platformAdapter)

orchestrator.supports('cart') // true (from platformAdapter)

Universal domains (catalog, store) always come from the primary orchestrator.

Notification Providers

Notification providers subscribe to commerce events and auto-dispatch through configured channels:

import type { NotificationProvider } from '@commercejs/types'

const resend: NotificationProvider = {
  id: 'resend',
  name: 'Resend',
  channels: ['email'],
  send: async (channel, message) => {
    await resendClient.emails.send({
      from: 'store@example.com',
      to: message.to,
      subject: message.subject,
      html: message.html,
    })
    return { success: true }
  },
}

const commerce = createCommerce({
  adapter,
  notifications: { resend },
  notificationRules: [
    {
      event: 'order.created',
      channel: 'email',
      provider: 'resend',
      template: 'order_confirmation',
      buildMessage: (payload) => ({
        to: payload.order.customer.email,
        subject: `Order #${payload.order.id} confirmed`,
        data: { order: payload.order },
      }),
    },
  ],
})

Notification failures are non-fatal — they're caught silently to avoid disrupting commerce operations.

Analytics Auto-Tracking

Analytics providers automatically receive all commerce events:

import type { AnalyticsProvider } from '@commercejs/types'

const ga4: AnalyticsProvider = {
  id: 'ga4',
  name: 'Google Analytics 4',
  track: (event, properties) => gtag('event', event, properties),
  identify: (userId, traits) => gtag('set', 'user_properties', { user_id: userId, ...traits }),
  page: (name, properties) => gtag('event', 'page_view', { page_title: name, ...properties }),
}

const commerce = createCommerce({
  adapter,
  analytics: [ga4],
})

// ga4.track() is called automatically for every commerce event
// (order.created, cart.item.added, payment.confirmed, etc.)

Analytics failures are non-fatal — they never block commerce operations.