Highlights
Shared Product Option Groups
Option groups are now shared resources — a single "Size" group can be linked to as many products as you need. They're also channel-aware, so multi-tenant setups can scope them per channel. There's a new dedicated management page in the dashboard, and CSV imports handle shared groups out of the box. A migration helper is provided for the data transition.
API Key Authentication
First-class API key support for machine-to-machine authentication. Create a key, scope it to specific permissions, use it in a header. Keys can be rotated and revoked at any time, with a full management UI in the dashboard. Community contribution from Daniel Biegler.
Translatable Assets
Asset now implements the Translatable interface — asset names and custom fields can vary per language. A migration helper copies existing names into translations automatically.
Configurable Order Tax Calculation
Order-level tax calculation is now pluggable via OrderTaxCalculationStrategy. Ships with DefaultOrderTaxCalculationStrategy (same as v3.5) and OrderLevelTaxCalculationStrategy.
Refreshed Dashboard
The dashboard is now built on @vendure-io/ui, our own open-source design system. Same tech stack, refined visual identity. Under the hood, headless primitives migrated from Radix UI to Base UI — all components now live in a single package. New extension points: toolbar items, function-based nav sections, component-based alert actions, improved ActionBar. Translation fallback placeholders for non-default languages. New Hungarian and Dutch translations.
Community Plugins
Several plugins have moved to the @vendure-community npm org with independent versioning. See the migration guide below for the full mapping.
Per-Queue Job Concurrency
Job queue concurrency now accepts a function (queueName: string) => number for per-queue control.
Other Notable Features
BootstrappedEvent— fires when the server is fully ready afterapp.listen()onBeforeAppListenhook — operate on the NestJS app before it starts listening- Async email generators
setOrderCurrencyCodeShop API mutation- Collection search filters (
collectionIds,collectionSlugs) - Braintree multi-currency support
- Custom field
dashboard: { visible: false }option - Settings Store management page
- Force update payment status
EntityAccessControlStrategy(developer preview) — row-level access control
Notable Fixes
- Dashboard compilation: 2x faster builds, 4x lower memory usage (replaced
ts.createProgramwith per-file transpilation) - Atomic, concurrency-safe
mergeOrders - Schema-qualified table paths in
EXISTSsubqueries (multi-schema Postgres fix) - Stale shipping line cleanup for deleted shipping methods
Migration Guide
This guide covers all breaking changes and required migration steps when upgrading from v3.5.x to v3.6.0.
1. Database Migration
Back up your database before proceeding. The migration involves moving data between tables, and while the helpers are idempotent and well-tested, a backup is always good practice before a schema change of this scale.
After updating your Vendure packages to v3.6, generate your migration:
npx vendure migrate --generate v36This generates a migration file in your configured migrations directory. Before starting your application, you need to edit the generated migration file to insert two data migration helpers. If you skip these steps, the auto-generated DDL will drop columns and data will be permanently lost.
1a. Asset Translation Data
The Asset entity is now translatable (#4171). The name column moves from the asset table to a new asset_translation table.
Open your generated migration file and look for the SQL that creates the asset_translation table. Further down in the same file, you'll find a statement that drops the name column from asset. Insert the migrateAssetTranslationData helper call between these two operations.
1b. Shared ProductOptionGroup Data
ProductOptionGroup and ProductOption are now shared resources that can belong to multiple products, and are channel-aware (#4469). The productId FK column on product_option_group is replaced by join tables.
Look for the SQL that creates the new join tables (e.g. CREATE TABLE "product_option_groups_product_option_group"). Further down, you'll find a statement that drops the productId column. Insert the migrateProductOptionGroupData helper call between these two operations.
Putting it together
Here is a real example of a generated Postgres migration with the helpers inserted. Your migration will look similar — the exact SQL and constraint names will vary, but the structure is the same.
import { MigrationInterface, QueryRunner } from 'typeorm';
import {
migrateAssetTranslationData,
migrateProductOptionGroupData,
} from '@vendure/core';
export class V361774950673940 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
// ... auto-generated DDL ...
await queryRunner.query(`CREATE TABLE "asset_translation" ("createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "languageCode" character varying NOT NULL, "name" character varying NOT NULL, "id" SERIAL NOT NULL, "baseId" integer, CONSTRAINT "PK_2f22e63eefeef14d245bdb956b6" PRIMARY KEY ("id"))`, undefined);
await queryRunner.query(`CREATE INDEX "IDX_4eed4464adef51f53e1c7d8021" ON "asset_translation" ("baseId") `, undefined);
// ... api_key tables, product_option join tables, etc. ...
await queryRunner.query(`CREATE TABLE "product_option_groups_product_option_group" ("productId" integer NOT NULL, "productOptionGroupId" integer NOT NULL, CONSTRAINT "PK_6a7a0291e226fbb0d4df828a483" PRIMARY KEY ("productId", "productOptionGroupId"))`, undefined);
// ... indexes ...
// *** INSERTED: migrate option group data before productId is dropped ***
await migrateProductOptionGroupData(queryRunner);
await queryRunner.query(`ALTER TABLE "product_option_group" DROP COLUMN "productId"`, undefined);
// *** INSERTED: migrate asset names before name column is dropped ***
await migrateAssetTranslationData(queryRunner);
await queryRunner.query(`ALTER TABLE "asset" DROP COLUMN "name"`, undefined);
// ... remaining DDL (foreign key constraints, etc.) ...
}
public async down(queryRunner: QueryRunner): Promise<any> {
// Auto-generated reverse DDL (no changes needed here)
}
}Tip: In a Postgres migration, look for the two DROP COLUMN lines — they'll be close together:
ALTER TABLE "product_option_group" DROP COLUMN "productId"
ALTER TABLE "asset" DROP COLUMN "name"
Insert each helper call immediately before its corresponding DROP COLUMN.
Note for SQLite users: SQLite doesn't support DROP COLUMN directly. TypeORM uses a temporary table pattern instead (create temp table without the column, copy data, drop original, rename). The same principle applies — insert the helpers before the data copy that omits the column — but the SQL will look different.
Both helpers are idempotent — safe to run multiple times without creating duplicate data. If the migration is interrupted partway through, you can re-run it safely.
Run the Migration
Start your application. The default index.ts scaffold calls runMigrations(config) before bootstrap(config), so the migration will be applied automatically on startup.
Alternatively, you can run it explicitly:
npx vendure migrate --run2. Community Plugins: New Package Names
Several plugins have been moved out of the Vendure monorepo into a dedicated community repo: vendurehq/vendure-community-plugins. They are now published under the @vendure-community npm org with independent versioning, decoupled from Vendure Core releases.
The plugin APIs are unchanged — same classes, same configuration, same behaviour. This is a package rename, not a rewrite.
If you use any of these plugins, uninstall the old package and install the new one:
| Old package (final version) | New package |
|---|---|
@vendure/elasticsearch-plugin@3.5.6
| @vendure-community/elasticsearch-plugin@1.1.0
|
@vendure/payments-plugin@3.5.6 (Stripe)
| @vendure-community/stripe-plugin@1.0.0
|
@vendure/payments-plugin@3.5.6 (Braintree)
| @vendure-community/braintree-plugin@1.0.0
|
@vendure/payments-plugin@3.5.6 (Mollie)
| @vendure-community/mollie-plugin@1.0.0
|
@vendure/sentry-plugin@3.5.6
| @vendure-community/sentry-plugin@1.0.0
|
@vendure/stellate-plugin@3.5.6
| @vendure-community/stellate-plugin@1.0.0
|
@vendure/job-queue-plugin@3.5.6 (pub-sub)
| @vendure-community/pub-sub-plugin@1.0.0
|
For example, to migrate the Stripe plugin:
npm uninstall @vendure/payments-plugin
npm install @vendure-community/stripe-plugin@1.0.0Then update all imports in your TypeScript code:
// Before
import { ElasticsearchPlugin } from '@vendure/elasticsearch-plugin';
import { StripePlugin } from '@vendure/payments-plugin/package/stripe';
// After
import { ElasticsearchPlugin } from '@vendure-community/elasticsearch-plugin';
import { StripePlugin } from '@vendure-community/stripe-plugin';Search your codebase for any remaining references to the old package names — this includes imports, dynamic require() calls, and any configuration files that reference the old packages.
Note: @vendure/job-queue-plugin (BullMQ) remains in core and continues to be published as part of Vendure. Only the pub-sub strategy has moved.
3. ElasticSearch Plugin: Upgrade to v9.1.0
If you use the ElasticSearch plugin (now @vendure-community/elasticsearch-plugin), you must upgrade your ElasticSearch instance from v7.x to v9.1.0 (#3740).
This is a one-way upgrade with no downgrade path. Test in a staging environment first.
Steps:
- Upgrade your ElasticSearch instance to v9.1.0. If running via Docker, update your image tag (e.g.
elasticsearch:9.1.0). - Update your project's
package.json:"@elastic/elasticsearch": "9.1.0"
- The ES database schema migration happens automatically on instance startup.
- After the upgrade, run a full reindex via the Admin API or
npx vendure reindexto rebuild the search index with the new client.
The old v7 client is incompatible with v9 and vice versa. Both the plugin and your project must use the v9.1.0 client.
4. Dashboard: Radix UI → Base UI Migration
The dashboard has migrated from Radix UI to Base UI via the new @vendure-io/ui package (#4531).
If you have custom dashboard extensions using Radix UI components, run the provided codemod:
npx vendure codemod dashboard-base-ui <target-directory>Important: The codemod scans all .tsx files reachable from the tsconfig. In a monorepo with multiple apps (e.g. a Next.js storefront alongside the Vendure server), running it from the repo root will also transform files in non-dashboard projects. This can break those projects — for example, rewriting import { toast } from 'sonner' to import { toast } from '@vendure/dashboard' in a Next.js app.
To avoid this:
- Run the codemod only on your dashboard extension directories, not the entire repo root. For example:
npx vendure codemod dashboard-base-ui libs/my-dashboard-plugin/ npx vendure codemod dashboard-base-ui src/plugins/
- If you do run it from the repo root, review changes in non-dashboard projects and revert them.
The codemod handles import path updates automatically. Component props that differ between Radix and Base UI may need manual adjustment. After running the codemod, check for any remaining TypeScript errors:
npx tsc --noEmitIf you don't have custom dashboard extensions, no action is needed.
5. Dashboard: Vite v6 → v7
The dashboard's Vite version has been upgraded from v6 to v7 (#4514).
No action required in the typical case. In monorepo environments where other projects depend on a different Vite version, review for potential version conflicts.
6. API & Type Changes
ProductOptionGroups Query — Now Paginated
The productOptionGroups query now returns a paginated ProductOptionGroupList (with items and totalItems) instead of a flat array. It accepts standard ProductOptionGroupListOptions for filtering, sorting, and pagination.
Before (v3.5):
query {
productOptionGroups(filterTerm: "size") {
id
name
}
}After (v3.6):
query {
productOptionGroups(options: {
filter: { name: { contains: "size" } }
}) {
items {
id
name
}
totalItems
}
}The old filterTerm parameter is replaced by the standard filter option, which supports all the usual operators (eq, contains, in, etc.).
Asset Type — Now Translatable
The Asset GraphQL type now includes languageCode and translations fields. CreateAssetInput and UpdateAssetInput accept an optional translations array.
Existing code that reads asset.name will continue to work — the name is resolved from the translation matching the current request language.
OrderMergeStrategy — Now Async
OrderMergeStrategy.merge() return type has been widened to MergedOrderLine[] | Promise<MergedOrderLine[]> (#4436). Existing synchronous implementations still work without changes. If you call merge() directly in custom code, you must now await the result:
// Before
const result = myStrategy.merge(ctx, order, guestOrder);
// After
const result = await myStrategy.merge(ctx, order, guestOrder);Job Queue Concurrency Type Widened
PollingJobQueueStrategy.concurrency type changed from number to number | ((queueName: string) => number) (#4201). The same applies to BullMQ and PubSub strategies. Existing numeric values still work without changes.
7. Deprecations
Health Check Features
The built-in health check features have been deprecated (#4442):
HttpHealthCheckStrategy,TypeOrmHealthCheckStrategy, andHealthCheckRegistryServiceare marked@deprecated- The health check page has been removed from the dashboard
- The health check HTTP endpoint still functions but will be removed in a future major version
We recommend migrating to a dedicated health check solution (e.g. Kubernetes liveness/readiness probes, or a custom NestJS health module using @nestjs/terminus directly).
8. New Opt-In Features
These are new features that don't require migration, but you may want to be aware of:
Anonymous Telemetry
v3.6 adds opt-out anonymous telemetry. It collects non-identifying usage data (strategy types, plugin adoption, feature flags) to help prioritize development. To disable, set the environment variable:
VENDURE_DISABLE_TELEMETRY=trueDefault Search Plugin: Currency Code Index
If you want to index by currency code in the default search plugin, you can now opt in (#3268):
DefaultSearchPlugin.init({
indexCurrencyCode: true,
})This changes the primary key of the search index table and requires a full reindex after enabling.
9. Dependency Updates
The following dependencies of @vendure/core have been updated (#4564). You only need to take action if your project directly imports or depends on these packages. If you only interact with them through Vendure's APIs, no changes are needed.
| Package | From | To |
|---|---|---|
bcrypt
| 5.x | 6.x |
better-sqlite3
| 11.x | 12.x |
mime-types
| 2.x | 3.x |
csv-parse
| 5.x | 6.x |
i18next
| 24.x | 25.x |
image-size
| 1.x | 2.x |
@graphql-tools/stitch
| 9.x | 10.x |
Note: bcrypt is a native addon. After upgrading, you may need to run npm rebuild bcrypt if you encounter errors.
Quick Checklist
- Back up your database
- Update all
@vendure/*packages to v3.6.0 - Replace community plugin packages (
@vendure/elasticsearch-plugin→@vendure-community/elasticsearch-plugin, etc.) - Update all imports in TypeScript code to use new community plugin package names
- Generate migration:
npx vendure migrate --generate v36 - Edit migration file: add
migrateProductOptionGroupData(queryRunner)beforeDROP COLUMN "productId" - Edit migration file: add
migrateAssetTranslationData(queryRunner)beforeDROP COLUMN "name" - Start application (or run
npx vendure migrate --run) to apply migration - If using ElasticSearch: upgrade instance to v9.1.0, update
@elastic/elasticsearchto9.1.0, run full reindex - If using custom dashboard extensions: run
npx vendure codemod dashboard-base-ui <target-dir>thennpx tsc --noEmit - If using
productOptionGroupsquery: update to paginated response format - If enabling
indexCurrencyCode: run full reindex - Run
npm rebuild bcryptif needed
What's Changed
- feat(default-search-plugin): add support for 'currencyCode' index by @casperiv0 in #3268
- feature(elasticsearch-plugin): adds search options by collection slugs or collection IDs by @alexisvigoureux in #3182
- feat(core): Add collectionIds and collectionSlugs filters to default search plugin by @dlhck in #3945
- refactor: Update e2e tests to use gql.tada features by @HouseinIsProgramming in #3974
- feat(asset-server-plugin): Allow specifying encoding for AssetStorageStrategy by @pujux in #3926
- feat(core): API Keys by @DanielBiegler in #3815
- chore: Update the Vendure ElasticSearch plugin to use ElasticSearch v9.1.0 by @LeftoversTodayAppAdmin in #3740
- chore: Update the Vendure ElasticSearch plugin to use ElasticSearch v9.1.0 by @biggamesmallworld in #4009
- feat(dashboard): Improved extensibility of ActionBar by @michaelbromley in #4049
- feat: Add support for asynchronous email generators by @twlite in #3976
- Mollie: Allow forcefully updating payment status in case webhooks are delayed by @martijnvdbrug in #4104
- 3909 pvp custom fields not showing by @mehringer68 in #4180
- feat(core)!: Make Asset entity translatable by @biggamesmallworld in #4171
- feat(core): Add anonymous telemetry collection module by @dlhck in #4192
- feat(core,job-queue-plugin): Add per-queue concurrency configuration by @biggamesmallworld in #4201
- chore(repo): Back-merge master into minor (conflicts) by @github-actions[bot] in #4396
- fix(core): Fix broken e2e test after master merge by @michaelbromley in #4399
- chore(repo): Back-merge master into minor (conflicts) by @github-actions[bot] in #4429
- chore(repo): Back-merge master into minor (conflicts) by @github-actions[bot] in #4433
- feat(core): Add EntityAccessControlStrategy for row-level access control by @michaelbromley in #4451
- chore(repo): Back-merge master into minor (conflicts) by @github-actions[bot] in #4465
- feat(core): Add Shop API mutation to set order currency code by @michaelbromley in #4466
- feat(core): Provide app instance before app starts listening by @LucidityDesign in #4383
- feat(core): Make ProductOptionGroup & ProductOption shared and channel-aware by @michaelbromley in #4469
- feat(dashboard): Add Settings Store management page by @michaelbromley in #4473
- feat(payments-plugin): Add multi currency support for braintree plugin by @kkerti in #3239
- refactor(core,dashboard): Deprecate built-in health check features by @dlhck in #4442
- feat(dashboard): Add Option Groups management page by @michaelbromley in #4483
- chore(repo): Back-merge master into minor (conflicts) by @github-actions[bot] in #4489
- feat(core): Add configurable OrderTaxSummaryCalculationStrategy by @colinpieper in #4376
- feat(dashboard): Add toolbarItems extension point to app shell header by @michaelbromley in #4496
- feat(dashboard): Support function form for navSections by @michaelbromley in #4491
- feat(core): Support shared product option groups in CSV import by @michaelbromley in #4503
- feat(core): Introduce BootstrappedEvent to signal server readiness by @Draykee in #4498
- feat(core): Allow async
OrderMergeStrategyby @twlite in #4436 - feat(dashboard): Upgrade Vite from v6 to v7 by @dlhck in #4514
- fix(dashboard): Add shared-types and shared-utils to Vite optimizeDeps by @dlhck in #4520
- feat(dashboard): Allow component-based alert actions for hook access by @michaelbromley in #4526
- refactor(dashboard): Migrate from Radix UI to Base UI via @vendure-io/ui by @dlhck in #4531
- feat(cli): Add codemod command for Radix to Base UI dashboard migration by @dlhck in #4536
- fix(dashboard): Follow transitive dependencies in plugin discovery by @michaelbromley in #4545
- feat(dashboard): Translation fallback placeholders for translatable fields by @dlhck in #4549
- fix(core): Server-side translation field-level fallback for empty values by @dlhck in #4551
- fix(dashboard): Fix component styling regressions from Base UI migration by @michaelbromley in #4552
- chore(repo): Back-merge master into minor (conflicts) by @github-actions[bot] in #4555
- feat(core): Expand telemetry with strategy, integration and feature adoption data by @dlhck in #4554
- fix(core): Allow admin re-creation after soft-delete by @HouseinIsProgramming in #4543
- fix(dashboard): Hide dev mode ring offset when not hovered by @michaelbromley in #4558
- chore(repo): Back-merge master into minor (conflicts) by @github-actions[bot] in #4562
- chore(plugins): Update safe plugin dependencies by @michaelbromley in #4565
- chore(core): Dependency updates phases 1-3 by @michaelbromley in #4564
- fix(dashboard): Fix flaky path-alias test and sql.js lockfile resolution by @michaelbromley in #4567
- chore(repo): Back-merge master into minor (conflicts) by @github-actions[bot] in #4568
- chore(repo): Back-merge master into minor (conflicts) by @github-actions[bot] in #4573
- chore(repo): Back-merge master into minor (conflicts) by @github-actions[bot] in #4574
- fix(dashboard): Align styling with design system tokens by @michaelbromley in #4575
- chore(repo): Back-merge master into minor (conflicts) by @github-actions[bot] in #4579
- chore(repo): Back-merge master into minor (conflicts) by @github-actions[bot] in #4581
- feat(dashboard): Add API keys management UI by @michaelbromley in #4583
- fix(dashboard): Fix collection expand e2e test selector by @michaelbromley in #4585
- feat(core): Add migrateAssetTranslationData() helper for v3.6 upgrade by @michaelbromley in #4584
- chore(repo): Back-merge master into minor (conflicts) by @github-actions[bot] in #4587
- fix(core): Upgrade i18next to v26 to remove locize promotional message by @michaelbromley in #4588
- chore(repo): Back-merge master into minor (conflicts) by @github-actions[bot] in #4589
- fix(core): Restore minor-branch dependency versions after merge by @michaelbromley in #4590
- chore: Remove community plugins from monorepo by @michaelbromley in #4591
- fix: Align dashboard and job-queue-plugin versions to 3.5.6 by @michaelbromley in #4592
- docs: Add API keys developer guide by @michaelbromley in #4595
- fix(core): Add missing docs annotations and exports for v3.6 APIs by @michaelbromley in #4597
New Contributors
- @mehringer68 made their first contribution in #4180
- @LucidityDesign made their first contribution in #4383
Full Changelog: v3.5.6...v3.6.0