Skip to content

Switch commerce engine

Each commerce engine is one package of modules, and all three serve the same GraphQL contract from @evolve-framework/schemas. Switching engines means replacing that package in every backend service that composes it: the config class, the client factory, the request context, and the modules. The gateway and storefront keep working against the same schema, with the exceptions listed under What the storefront notices.

Engine Package Config class
commercetools @evolve-framework/commercetools CommercetoolsConfig
Medusa @evolve-framework/medusa MedusaConfig
Salesforce Commerce Cloud @evolve-framework/sfcc CommerceCloudConfig

There is no evolve add engine command. The steps below are the whole switch. Moving your data (products, categories, prices, customers, orders) from one engine to the other is a separate project, and this guide does not cover it.

Start by checking what your project uses against what the new engine serves.

  • Per operation. Each page in the GraphQL reference has a “Served by” list that says whether each engine resolves the operation, answers it with a stub, or has no resolver for it.
  • Per module. Each engine module’s reference page links the same module in the other engines, for example CartModule.

Where the engines differ, at the operation level:

Operation commercetools Medusa SFCC
createGuestSession Yes No resolver No resolver
refreshToken Yes No resolver Yes
customerPasswordUpdate Yes No resolver Stub
customerPasswordReset, customerPasswordResetRequest Yes Yes Stub
businessUnit, businessUnitSwitch, businessUnitAssociateInvite, businessUnitAssociateRemove, businessUnitAssociateRolesUpdate Yes Yes Stub
businessUnitAddressCreate, businessUnitAddressUpdate, businessUnitAddressDelete, businessUnitDefaultAddressUpdate, businessUnitAssociateActivate Yes No resolver No resolver
businessUnitAssociateDeactivate Yes Yes No resolver
cartSetDeliveryRemarks Yes No resolver No resolver
cartSetPurchaseOrderNumber Yes Not in schema Not in schema
cartCreateNamed, cartRename, cartSetCurrent, cartDelete Yes, with MultipleCartModule Not in schema Not in schema
orderRevise, orderCancel Yes Not in schema Not in schema
customerCreate, customerAddressCreate, customerAddressUpdate, customerAddressDelete, customerInformationUpdate Yes Yes Stub
customerDelete, customerEmailUpdate Yes No resolver Stub
customerDefaultAddressUpdate, customerPersonalDetailsUpdate Yes Yes No resolver
customerNotificationPreferencesUpdate Yes No resolver No resolver
orderByPublicId Yes Yes No resolver
orderApprovalRuleCreate, orderApprovalRuleUpdate, orderApprovalRuleDelete, orderApprovalStatusUpdate Yes Yes Not in schema
quoteRequestCreate, quoteRequestCancel Yes Yes No resolver
applePayPaymentValidate Yes Always an error Always an error
shoppingListCreate, shoppingListUpdate, shoppingListDelete, shoppingListLineItemsAdd, shoppingListLineItemsUpdate, shoppingListLineItemsRemove Yes Yes Stub
Customer.invoices, BusinessUnit.invoices Yes, with InvoiceModule Not in schema Not in schema
  • Stub: the operation is in the schema and answers “Not implemented yet”. SFCC has 27 of them: the operations above, plus five BusinessUnit fields (such as associates and billingAddress) and BusinessUnitAssociate.roles.
  • No resolver: the operation is in the schema, but nothing answers it. A mutation fails with a GraphQL error, because its result is non-null. A nullable query, such as orderByPublicId on SFCC, returns null.
  • Not in schema: no module of that engine composes it, so a document that selects it fails validation.

Everything else, from product and productSearch to cart, orderPlace, and paymentCreate, all three engines resolve. One query, addressValidate, has no resolver on any engine.

Do this in every backend service that composes engine modules. In the reference sites those are backend/discover (catalog) and backend/purchase (everything from the cart on).

  1. Prepare the engine. Each engine needs something before the modules can serve the contract.

    • commercetools: an API client (CTP_CLIENT_ID) and, for customer sessions, a user API client (CTP_USER_CLIENT_ID); the custom types your customTypes keys name; and the payment states listed in Add a payment provider.
    • Medusa: a publishable key per store, Medusa’s Translation Module enabled (featureFlags: { translation: true }), and the Evolve Medusa plugins @evolve-framework/medusa-plugin-slug, -company, -quote, -approval, -wishlist, -customer-lookup, and -events. The business unit and shopping list modules fail on their first call without their plugin, not at startup. The package holds no admin credentials: what the Store API does not expose comes from a plugin. See the package README.
    • SFCC: a Shopper API client for one site (SFCC_SITE_ID).
  2. Replace the package. Remove the old engine package and its add-ons (the commercetools-* payment packages run only on commercetools), and install the new one:

    pnpm remove @evolve-framework/commercetools
    pnpm add @evolve-framework/medusa
  3. Load the config and build the client factory. Each engine reads its own variables. Extend the class with your service’s own variables as described in Configuration.

    import {
    ClientFactory,
    CommercetoolsConfig,
    getClientOptions,
    getTokenManagerOptions,
    TokenManager,
    } from "@evolve-framework/commercetools";
    export const config = new CommercetoolsConfig();
    export let clientFactory!: ClientFactory;
    export const tokenManager = new TokenManager();
    export const initEngine = async (): Promise<void> => {
    await config.load();
    clientFactory = ClientFactory.create(await getClientOptions(config));
    tokenManager.configure(await getTokenManagerOptions(config));
    };

    CTP_PROJECT_KEY, CTP_API_URL, CTP_AUTH_URL, CTP_CLIENT_ID, CTP_CLIENT_SECRET, CTP_SCOPES, and for the TokenManager CTP_USER_CLIENT_ID, CTP_USER_CLIENT_SECRET, and optionally CTP_USER_SCOPES.

  4. Rewrite the request context. The headers a service receives stay the same, but each engine has its own StoreContext and ClientContext. Authentication and sessions explains how each engine models a session.

    The commercetools context factory is the one in Authentication and sessions: a StoreContext around the system client, and a ClientContext with a CommercetoolsClientLoader that holds the TokenManager.

    The plugins stay as they are: useFederatedToken(), then useClientContext().

  5. Swap the modules. A module that exists in more than one package has the same name in each, but takes different options. The commercetools modules take the client factory, your custom type keys, and the payment registry; the Medusa modules take almost nothing.

    Module commercetools Medusa SFCC
    AuthModule { config, clientFactory } none { config, clientFactory }
    BusinessUnitModule { roleCapabilities? } none none (mostly stubs)
    CatalogModule { clientFactory, search?, variantDimensions? } { search?, specifications? } { clientFactory }
    CartModule { clientFactory, customTypes, payments? } none none
    CheckoutModule { clientFactory, payments?, orderCreationStrategy?, quotes?, approvals?, revision? } none none
    CustomerModule { config, customTypes } none none
    OrderModule { clientFactory, payments?, trackingUrls? } none none
    OrderApprovalModule none none Not available
    PaymentModule { clientFactory, customTypes, payments?, routePrefix? } none none
    QuoteModule, ShoppingListModule none none none
    InvoiceModule, MultipleCartModule, ApiExtensionModule, PaymentProviderModule Yes Not available Not available

    The commercetools modules import from @evolve-framework/commercetools, the others from @evolve-framework/medusa/modules and @evolve-framework/sfcc/modules. Type the CompositeModule with the engine’s context: CommercetoolsLoaderContext, ContextValue from @evolve-framework/medusa, or SfccLoaderContext.

    Some modules cannot be left out, or the subgraph does not compose:

    • BusinessUnitModule, even in a B2C service: the customer, order, and shopping list types of every engine reference BusinessUnit.
    • On Medusa, ShoppingListModule next to CustomerModule, and BusinessUnitModule next to QuoteModule and OrderApprovalModule.
  6. Move payments. The payment packages (commercetools-adyen, -stripe, -mollie, -paynl, -buckaroo, -pay-on-account) and PaymentRegistry exist only for commercetools. On the other engines, the engine takes the payment:

    • Medusa offers the providers of Medusa’s own payment module that are linked to the cart’s region. orderPlace completes only a cart whose payment Medusa has authorized, so paymentCreate on the cart comes first. A checkout that places the order before paying needs to change.
    • SFCC takes the money through its own server-side payment integration. paymentCreate records the chosen method on the basket or order and sends the shopper nowhere. SFCC keeps no invoices.
  7. Move search. Each engine answers productSearch itself when you pass no search provider. To answer it from a search vendor instead:

    • commercetools: pass search: new DocumentSearchProvider({ adapter }) to CatalogModule, and index with CommercetoolsCatalogSource from @evolve-framework/commercetools/catalog.
    • Medusa: pass search: new ProductSearchProvider({ adapter }) from @evolve-framework/medusa/search, and index with MedusaProductDocumentSource. Medusa’s own search returns no facet counts.
    • SFCC: CatalogModule takes no search provider, so search stays with SFCC.

    The query adapters from search-algolia and search-meilisearch work with both providers. See Add search.

  8. Reconnect events. Only the commercetools package turns engine events into the internal events of React to events, with createSubscriptionHandler. CMS catalog pages and search indexing that follow those events stop when you leave commercetools. On Medusa, medusa-plugin-events forwards an event type and an id to an HTTP route that you write in a module of your own. SFCC publishes no events.

  9. Port your own code. Anything that talks to the engine directly is engine-specific: resolvers that call clientContext.getClient(), the reference sites’ extensions modules, commercetools API extensions, custom types, and the configuration/commercetools Terraform. The evolve simulate and evolve cleanup commands come from the commercetools package and leave with it. The Medusa client context has no hasToken(), so assertActiveSession does not work there; see Add your own module.

  10. Regenerate and run. Regenerate the subgraph schemas, recompose the supergraph, and run the storefront’s codegen. In the reference sites, pnpm codegen at the root runs all three.

The storefront talks to the gateway, and the gateway carries one session format for every engine, so most of the storefront does not change. Three things do.

Sessions start over. The federated token holds each engine’s session under its own name: commercetools, medusa, or commercecloud. A shopper’s commercetools session means nothing to Medusa, so after the switch every shopper is a guest until they sign in again, and open carts stay behind in the old engine. The reference sites’ gateway config names the engine session the gateway takes the token subject from with FEDERATED_TOKEN_SUBJECT_KEY, which defaults to commercetools. Set it to the new engine’s name.

Some documents no longer validate. The storefront’s codegen validates every feature package’s documents against the supergraph. With the documents of feature-cart 0.12 and feature-order 0.14, these fail:

Selected field Where Fails on
Cart.purchaseOrderNumber feature-cart’s cart fragment, which GetCart and the cart mutations select Medusa, SFCC
cartSetPurchaseOrderNumber CartSetPurchaseOrderNumber Medusa, SFCC
orderRevise, orderCancel OrderRevise, OrderCancel in feature-order Medusa, SFCC
OrderPlaceResponse.approval CartOrderPlace, which places every order SFCC
Order.approval feature-order’s order detail fragment SFCC
Cart.name, Cart.owner, cartCreateNamed, and the other named-cart fields GetCarts and the named-cart mutations Any engine without MultipleCartModule

The reference sites already filter out whole operations that the supergraph cannot serve, in frontend/site/codegen/feature-documents.cjs. Add CartSetPurchaseOrderNumber, OrderRevise, and OrderCancel to its unreachableOperations.

A field inside a shared fragment cannot be filtered that way. Until the engine composes it, declare it in a module of your own in the service that owns the cart, and answer null:

import { AbstractModule } from "@evolve-framework/core";
import { gql } from "graphql-tag";
export class ContractGapsModule extends AbstractModule {
protected override typedefs = gql`
extend type Cart {
purchaseOrderNumber: String
}
`;
protected override resolvers = {
Cart: { purchaseOrderNumber: () => null },
};
}

On SFCC, the two approval fields need the same treatment. Their types are in @evolve-framework/schemas/graphql/order-approval.

Stubs and missing resolvers fail at run time. A document that validates can still fail when it runs: a stub answers “Not implemented yet”, and a mutation without a resolver fails with a GraphQL error. Hide the matching features, such as account editing and shopping lists on SFCC, before you go live. The table under See what changes lists them.