Important: Always upgrade to the latest patch version of each major version before upgrading to the next major version.
BREAKING CHANGE: All deprecated adapter exports and registration methods have been removed. You must use the new unified plugin architecture.
All old adapter exports from plugin files have been removed. Use the new *Plugin exports instead:
// ❌ REMOVED - Old adapter exports
import { Invoice } from '@unchainedshop/plugins/payment/invoice.ts';
import { Stripe } from '@unchainedshop/plugins/payment/stripe/index.js';
import { Post } from '@unchainedshop/plugins/delivery/post.ts';
import { GridFS } from '@unchainedshop/plugins/files/gridfs/index.js';
// ✅ USE - New plugin exports
import { InvoicePlugin } from '@unchainedshop/plugins/payment/invoice.ts';
import { StripePlugin } from '@unchainedshop/plugins/payment/stripe/index.js';
import { PostPlugin } from '@unchainedshop/plugins/delivery/post.ts';
import { GridFSPlugin } from '@unchainedshop/plugins/files/gridfs/index.js';The registerAdapter() method on all Directors has been removed. Plugins are now registered via the preset functions or pluginRegistry:
// ❌ REMOVED
import { PaymentDirector } from '@unchainedshop/core';
import { StripePlugin } from '@unchainedshop/plugins/payment/stripe/index.js';
PaymentDirector.registerAdapter(StripePlugin);
// ✅ USE - Register via preset functions
import { registerAllPlugins } from '@unchainedshop/plugins/presets/all.js';
registerAllPlugins(); // Registers all plugins including Stripe
// ✅ OR USE - Direct plugin registry for custom setups
import { pluginRegistry } from '@unchainedshop/core';
import { StripePlugin } from '@unchainedshop/plugins/payment/stripe/index.js';
pluginRegistry.register(StripePlugin);Affected Directors:
- PaymentDirector
- DeliveryDirector
- FileDirector
- WarehousingDirector
- WorkerDirector
- FilterDirector
- QuotationDirector
- EnrollmentDirector
- ProductPricingDirector
- ProductDiscountDirector
- OrderPricingDirector
- OrderDiscountDirector
- PaymentPricingDirector
- DeliveryPricingDirector
Default exports from preset modules have been removed. Use named registration functions:
// ❌ REMOVED
import defaultModules from '@unchainedshop/plugins/presets/base.js';
// ✅ USE - Import named registration function
import { registerBasePlugins } from '@unchainedshop/plugins/presets/base.js';
registerBasePlugins();registerBasePlugins()- Essential plugins (from@unchainedshop/plugins/presets/base.js)registerAllPlugins()- All available plugins (from@unchainedshop/plugins/presets/all.js)registerCryptoPlugins()- Cryptocurrency plugins (from@unchainedshop/plugins/presets/crypto.js)
The following deprecated mutations have been completely removed. Use the new cart-based mutations instead:
// ❌ REMOVED
setOrderDeliveryProvider(orderId: ID!, deliveryProviderId: ID!)
setOrderPaymentProvider(orderId: ID!, paymentProviderId: ID!)
updateOrderDeliveryShipping(orderId: ID!, address: AddressInput, meta: JSON)
updateOrderDeliveryPickUp(orderId: ID!, orderPickUpLocationId: String, meta: JSON)
updateOrderPaymentInvoice(orderId: ID!, paymentContext: JSON, meta: JSON)
updateOrderPaymentGeneric(orderId: ID!, paymentContext: JSON, meta: JSON)
updateOrderPaymentCard(orderId: ID!, paymentContext: JSON, meta: JSON) // Removed in v4
// ✅ USE - New cart mutations
updateCart(orderId: ID!, deliveryProviderId: ID, paymentProviderId: ID, ...)
updateCartDeliveryShipping(orderId: ID!, address: AddressInput, meta: JSON)
updateCartDeliveryPickUp(orderId: ID!, orderPickUpLocationId: String, meta: JSON)
updateCartPaymentInvoice(orderId: ID!, paymentContext: JSON, meta: JSON)
updateCartPaymentGeneric(orderId: ID!, paymentContext: JSON, meta: JSON)// ❌ REMOVED
OrderDeliveryPickUp.pickUpLocations
// ✅ USE - Access via DeliveryProvider
DeliveryProvider.pickupLocationsDeprecated router aliases have been removed:
// ❌ REMOVED
import { expressRouter } from '@unchainedshop/api/express';
import { fastifyRouter } from '@unchainedshop/api/fastify';
// ✅ USE
import { adminUIRouter } from '@unchainedshop/api/express';
import { adminUIRouter } from '@unchainedshop/api/fastify';BREAKING CHANGE: The PayPal Checkout plugin has been completely removed because the underlying SDK (@paypal/checkout-server-sdk) has been deprecated by PayPal.
// ❌ REMOVED
import { PaypalCheckoutPlugin } from '@unchainedshop/plugins/payment/paypal-checkout-plugin.ts';Migration Options:
- Use Braintree plugin (supports PayPal via Braintree)
- Implement custom PayPal integration using
@paypal/paypal-server-sdk(new official SDK) - Use alternative payment providers
BREAKING CHANGE: PluginRegistry.registerAdapters() method removed (was a no-op).
If you were calling this method, simply remove it. Adapters are now registered via pluginRegistry.register() or preset functions.
- Add
UNCHAINED_DOCUMENTDB_COMPAT_MODEfor FerretDB/AWS/Azure DocumentDB compatibility - Breaking:
UNCHAINED_TOKEN_SECRETnow requires a minimum of 32 characters. Update your secret if it was shorter in previous versions
When using the Express adapter (@unchainedshop/api/express), ensure you have the correct peer dependency versions:
npm install multer@">=2 <3" passport@">=0.7 <1" passport-strategyFor ticketing/crypto functionality:
npm install @scure/bip32@">=2" @scure/btc-signer@">=2"Migration IDs must now be between 19700000000000 and 99999999999999 (14 digits max). If you have existing migrations with 15-digit IDs (e.g., 202409302329000), you'll need to update them.
Recommended format: YYYYMMDDHHmmss (e.g., 20240930232900)
The MongoDB driver now throws errors instead of returning error objects for certain operations:
// Collection.dropIndex() now throws when index doesn't exist
- const result = collection.dropIndex('my_index');
- if (result.errmsg) { /* handle error */ }
+ try {
+ await collection.dropIndex('my_index');
+ } catch (error) {
+ // Handle missing index error
+ }Note: Ensure you await async operations to properly catch errors.
- context.currencyContext / countryContext / localeContext
+ context.currencyCode / countryCode / locale
- Price.currency / Order.currency
+ Price.currencyCode / Order.currencyCode
- simulatedPrice(currency: ...) / catalogPrice(currency: ...)
+ simulatedPrice(currencyCode: ...) / catalogPrice(currencyCode: ...)Update custom pricing plugins:
- ProductPricingSheet({ calculation, currency, quantity })
+ ProductPricingSheet({ calculation, currencyCode, quantity })When creating locale objects for API calls:
- locale: "de"
+ locale: new Intl.Locale("de")- token.chainTokenId
+ token.tokenSerialNumberAll enum values now use SCREAMING_SNAKE_CASE:
- ProductType.TokenizedProduct
+ ProductType.TOKENIZED_PRODUCT
- type: "simple" / "configurable" / "bundle" / "plan"
+ type: "SIMPLE" / "CONFIGURABLE" / "BUNDLE" / "PLAN"- import { BulkImportOperation } from '@unchainedshop/platform';
+ import { BulkImportOperation } from '@unchainedshop/core';
+ const handler: BulkImportOperation<unknown> = async (// v4: login() is now available directly on context
export default async function loginResolver(_, args, context: Context) {
const user = await context.modules.users.findUserByEmail(args.email);
return context.login(user);
}modules.users.createUser now accepts a plain password instead of a pre-hashed password:
- const hashedPassword = await modules.users.hashPassword(plainPassword);
- const user = await modules.users.createUser({ email, password: hashedPassword });
+ const user = await modules.users.createUser({ email, password: plainPassword });Removed: updateOrderPaymentCard
Deprecated (use new cart mutations instead):
- setOrderDeliveryProvider / setOrderPaymentProvider
- updateOrderDeliveryShipping / updateOrderDeliveryPickUp
- updateOrderPaymentInvoice / updateOrderPaymentGeneric
+ updateCartDeliveryShipping / updateCartDeliveryPickUp
+ updateCartPaymentInvoice / updateCartPaymentGeneric- events(created: DateTime) / eventsCount(created: DateTime)
+ events(created: DateFilterInput) / eventsCount(created: DateFilterInput)
- deliveryInterfaces(type: DeliveryProviderType!)
+ deliveryInterfaces(type: DeliveryProviderType) # type now optionalNew filters: Query.orders accepts paymentProviderIds, deliveryProviderIds, dateRange; Query.users accepts tags
- workQueueOptions: { retryInput: ... }
+ workQueueOptions: { transformRetry: ... }- Twilio:
SMS→TWILIO - Payment plugins:
paymentProviderIdremoved from adapter context - MCP/AI packages now optional peer dependencies
Plugin middlewares must now be wrapped in initPluginMiddlewares. When combining multiple plugin sets (e.g., base plugins with ticketing), wrap them together:
- connectBasePluginsToFastify(app);
- connectTicketingToFastify(app);
+ connect(fastify, platform, {
+ initPluginMiddlewares: (app) => {
+ connectBasePluginsToFastify(app);
+ connectTicketingToFastify(app);
+ }
+ });See working example: ticketing/boot.ts
The Admin UI is now packaged and served automatically when installed. Simply install the package and enable it:
npm install @unchainedshop/admin-uiconnect(fastify, platform, {
adminUI: true,
// ... other options
});See working example: kitchensink/src/boot.ts
- Add
UNCHAINED_TOKEN_SECRET(required)
npm install @graphql-yoga/plugin-response-cache graphql-yoga cookie
npm uninstall @apollo/server-plugin-response-cache @apollo/server apollo-graphiql-playgroundSome packages are now peer dependencies that must be installed manually:
# Required when using Express adapter (@unchainedshop/api/express)
npm install multer passport passport-strategy
# For ticketing/crypto functionality
npm install @scure/bip32 @scure/btc-signer- import { startPlatform, withAccessToken, connectPlatformToExpress4 } from '@unchainedshop/platform';
- import { defaultModules, connectDefaultPluginsToExpress4 } from '@unchainedshop/plugins';
+ import { startPlatform } from '@unchainedshop/platform';
+ import { connect } from '@unchainedshop/api/express';
+ import defaultModules from '@unchainedshop/plugins/presets/all.js';
+ import connectDefaultPluginsToExpress from '@unchainedshop/plugins/presets/all-express.js';
- const engine = await startPlatform({ ..., context: withAccessToken() });
- await engine.apolloGraphQLServer.start();
- connectPlatformToExpress4(app, engine, { corsOrigins: [] });
+ const engine = await startPlatform({ modules: defaultModules });
+ connect(app, engine, { initPluginMiddlewares: connectDefaultPluginsToExpress });See working example: kitchensink/src/boot.ts
import Fastify from 'fastify';
import { startPlatform } from '@unchainedshop/platform';
import { connect } from '@unchainedshop/api/fastify';
import defaultModules from '@unchainedshop/plugins/presets/all.js';
import initPluginMiddlewares from '@unchainedshop/plugins/presets/all-fastify.js';
const fastify = Fastify();
const platform = await startPlatform({ modules: defaultModules });
connect(fastify, platform, { initPluginMiddlewares });- options: { accounts: { ... } }
+ options: { users: { ... } }
// Password validation example:
- startPlatform({ accounts: { password: { validateUsername: () => true } } });
+ startPlatform({ options: { users: { validateUsername: async () => true } } });The @unchainedshop/types package has been removed. Import types from their respective packages:
| Old Import | New Import |
|---|---|
@unchainedshop/types/api.js → Context |
@unchainedshop/api |
@unchainedshop/types/common.js → ModuleInput |
@unchainedshop/mongodb |
@unchainedshop/types/common.js → TimestampFields |
@unchainedshop/mongodb |
@unchainedshop/types/user.js → User |
@unchainedshop/core-users |
@unchainedshop/types/orders.js → Order, OrderPosition |
@unchainedshop/core-orders |
@unchainedshop/types/products.js → Product |
@unchainedshop/core-products |
@unchainedshop/types/files.js → File |
@unchainedshop/core-files |
@unchainedshop/types/worker.js → IWorkerAdapter |
@unchainedshop/core |
@unchainedshop/types/pricing.js → pricing types |
@unchainedshop/core |
@unchainedshop/types/filters.js → IFilterAdapter, FilterContext |
@unchainedshop/core |
@unchainedshop/types/warehousing.js → TokenSurrogate |
@unchainedshop/core-warehousing |
@unchainedshop/types/events.js → OrderStatus |
@unchainedshop/core-orders |
Note: The Root type is no longer exported. Use unknown instead in resolver signatures.
All directors and adapters have been moved to @unchainedshop/core:
- import { WorkerDirector } from "@unchainedshop/core-worker";
- import { FilterDirector } from "@unchainedshop/core-filters";
- import { WarehousingDirector } from "@unchainedshop/core-warehousing";
+ import {
+ WorkerDirector,
+ FilterDirector,
+ WarehousingDirector,
+ WarehousingAdapter,
+ IWarehousingAdapter,
+ WarehousingContext,
+ IWorkerAdapter,
+ IFilterAdapter,
+ FilterContext,
+ TemplateResolver,
+ OrderPricingSheet,
+ OrderPricingRowCategory,
+ ProductPricingSheet,
+ } from "@unchainedshop/core";- import { checkAction } from "@unchainedshop/api";
- import { actions } from "@unchainedshop/roles";
+ import { acl, roles } from "@unchainedshop/api";
// Usage: acl.checkAction(), roles.actionsThe req object has been removed from context. Use the new getHeader method:
- export default async function myResolver(_, args, context: Context) {
- const headerValue = context.req.headers["x-custom-header"];
- }
+ export default async function myResolver(_, args, context: Context) {
+ const headerValue = context.getHeader("x-custom-header");
+ }- const user = await modules.accounts.findUserByEmail(email);
- const hash = hashPassword(password); // from @unchainedshop/api
+ const user = await modules.users.findUserByEmail(email);
+ const hash = await modules.users.hashPassword(password);- const url = modules.files.getUrl(file, params);
+ const url = file?.url && modules.files.normalizeUrl(file.url, params);modules.messaging.renderToText has been removed. Use a template library directly:
- const text = await modules.messaging.renderToText(template, data);
+ // Install mustache: npm install mustache @types/mustache
+ import Mustache from "mustache";
+ const text = Mustache.render(template, data);- const pricing = modules.orders.pricingSheet(order);
- const positionPricing = modules.orders.positions.pricingSheet(orderPosition);
+ import { OrderPricingSheet, ProductPricingSheet } from "@unchainedshop/core";
+
+ const pricing = OrderPricingSheet({
+ calculation: order.calculation,
+ currencyCode: order.currencyCode,
+ });
+
+ const positionPricing = ProductPricingSheet({
+ calculation: orderPosition.calculation,
+ currencyCode: order.currencyCode,
+ quantity: orderPosition.quantity,
+ });Note: OrderPositionPricingSheet has been renamed to ProductPricingSheet.
- import setupTicketing from "@unchainedshop/ticketing";
- setupTicketing(app, engine.unchainedAPI, { renderOrderPDF, createAppleWalletPass });
+ import setupTicketing, { TicketingAPI, ticketingModules } from "@unchainedshop/ticketing";
+ import connectTicketingToFastify from "@unchainedshop/ticketing/lib/fastify.js";
+ // or for Express:
+ // import connectTicketingToExpress from "@unchainedshop/ticketing/lib/express.js";
+ import ticketingServices from "@unchainedshop/ticketing/lib/services.js";
+
+ const platform = await startPlatform({
+ modules: { ...baseModules, ...ticketingModules },
+ services: { ...ticketingServices },
+ });
+
+ setupTicketing(platform.unchainedAPI as TicketingAPI, {
+ renderOrderPDF,
+ createAppleWalletPass,
+ createGoogleWalletPass,
+ });
+
+ // Connect in your middleware setup
+ connectTicketingToFastify(app);See working example: ticketing/boot.ts
loginWithOAuth,linkOAuthAccount,unlinkOAuthAccountlogoutAllSessionsbuildSecretTOTPAuthURL,enableTOTP,disableTOTPupdateUserAvatar,addProductMedia,addAssortmentMedia(use PUT upload)
- { id: String!, token: String!, tokenExpires, user }
+ { _id: String!, tokenExpires, user } # Use cookies/access-keys for auth- plainPassword / newPlainPassword / oldPlainPassword / totpCode
+ password / newPassword / oldPassword (totpCode removed)- createProduct(product: { title: "...", type: "..." })
+ createProduct(product: { type: "..." }, texts: [{ locale: "en", title: "..." }])Same pattern for: createProductVariation, createProductVariationOption, createAssortment, createFilter, createFilterOption
UpdateProductTextInput→ProductTextInputUpdateAssortmentTextInput→AssortmentTextInputUpdateFilterTextInput→FilterTextInput- All
localefields:String→Locale
Price._id,Stock._id,Dispatch._id,PriceRange._idUser.isTwoFactorEnabled,User.oAuthAccountsShop.oAuthProviders
- modules.orders.checkout(order)
+ services.orders.checkoutOrder(order)
- modules.orders.pricingSheet(order)
+ import { OrderPricingSheet } from '@unchainedshop/core';
+ OrderPricingSheet({ calculation: order.calculation, currencyCode: order.currencyCode })
- modules.accounts.findUserByEmail / setUsername / createUser
+ modules.users.findUserByEmail / setUsername / createUser
- modules.users.delete
+ services.users.deleteUser
- modules.filters.search.searchProducts
+ services.filters.searchProducts
- getOrderCart
+ services.orders.findOrInitCartaddMultipleCartProductsreturnsOrder!instead of[OrderItem]!removeUser(userId, removeUserReviews)has new optional parameter- Cart totals return
nullwhen cart is empty - Custom login: use
context.login(user)instead ofregisterLoginHandlers - Account events:
USER_UPDATE_PASSWORD,USER_ACCOUNT_ACTION
| Error | Solution |
|---|---|
Cannot find module '@unchainedshop/types/*' |
Types moved to respective packages (see table above) |
Property 'req' does not exist on type 'Context' |
Use context.getHeader() instead |
Module has no exported member 'checkAction' |
Use acl.checkAction() from namespace import |
Property 'accounts' does not exist |
Use modules.users instead |
Cannot find module '@unchainedshop/core-worker' |
Import directors from @unchainedshop/core |
Cannot find module 'multer' or 'passport' |
Install peer dependencies: npm install multer@">=2 <3" passport@">=0.7 <1" |
Property 'pricingSheet' does not exist on modules.orders |
Import OrderPricingSheet from @unchainedshop/core |
hashPassword is not a function |
Use modules.users.hashPassword() |
Property 'currency' does not exist (v4) |
Renamed to currencyCode |
ProductType.TokenizedProduct is undefined (v4) |
Use ProductType.TOKENIZED_PRODUCT |
PASSWORD_INVALID when using createUser (v4) |
Pass plaintext password, not pre-hashed. The module hashes internally now |
UNCHAINED_TOKEN_SECRET validation error (v4) |
Secret must be at least 32 characters |
| Migration ID validation error (v4) | Use 14-digit IDs max (format: YYYYMMDDHHmmss) |
dropIndex not catching errors |
Use await and try/catch - MongoDB driver now throws instead of returning error objects |
WHATWG Fetch support required. Update Node to 18+ or enable Experimental Fetch support on Node.js 16+.
npm install graphql@16
npm uninstall apollo-server-express body-parser graphql-scalars graphql-upload isomorphic-unfetch locale simpl-schemaRemove custom login-with-single-sign-on and all code that involves loading standard plugins and/or gridfs/datatrans webhooks.
startPlatform no longer hooks into Express or starts the GraphQL server automatically. This change supports other backend frameworks and Lambda Mode.
import { defaultModules, connectDefaultPluginsToExpress4 } from '@unchainedshop/plugins';
import { connect } from '@unchainedshop/api/express/index.js';
const engine = await startPlatform({ modules: defaultModules, /* ... */ });
await engine.apolloGraphQLServer.start();
connect(app, engine);
connectDefaultPluginsToExpress4(app, engine);The userId parameters used to set internal db fields (updatedBy / createdBy) have been removed from various functions. This will likely affect seed code. TypeScript will help identify affected locations.
Examine the API Breaking Changes in the Changelog for incompatibilities between 1.2 and 2.0.