2.2
Compass goes 4.0.0, SAP Commerce Cloud storefronts move to /c/ and /p/ URLs, Bloomreach Discovery restructures its middleware config
Showing every integration. Set your stack in Preferences and this page hides what isn't yours.
20.* || >=22.14.0Three changes in 2.2 need planning. @alokai/compass 4.0.0 is a major release, and it changes how you declare every workflow and every tool. SAP Commerce Cloud storefronts get native /c/ and /p/ URLs. The new page code doesn't reach an existing project, but the packages and module installers that do reach it already point at the new URLs, so a project that bumps them and stops there silently loses CMS content on its category and product pages. @vsf-enterprise/bloomreach-discovery-api 8.0.0 moves its resolvers and search settings.
Read three patch changes before you plan the upgrade. From 2.2.1, a normalizer validation failure returns your platform's raw payload to the browser. 2.2.2 stops injecting the script that carries your NEXT_PUBLIC_* values into the browser, and 2.2.3 fixes it, so 2.2.3 is the version to land on. 2.2.5 rewrites every circuit breaker log line, and a security fix redacts every log's metadata. The upgrade guide linked below collects every step for the line.
Highlights
Compass 4.0.0 rewrites how tools and workflows are declared
@alokai/compassCompass had two factories, defineTool and defineDynamicTool, for one job, and both took a single object with the callback inside it.
defineDynamicTool is gone, and defineTool replaces both factories. defineTool now takes two arguments, defineTool(config, callback), where at 2.1.3 both factories took a single object with callback inside it. The callback's second parameter is now params - { aiProvidedArgs, workflowPayload } - instead of the bare args. schema is required, and it can be a Zod schema or an async (context, params) => Promise<schema>.
Workflow.startingPrompt is renamed workflowPrompt. AnalyticsActionConfig collapses to { type: 'analytics' }, so it drops model, modelConfiguration, and toolkit. createGenrativeUiComponent is renamed createGenerativeUiComponent. Tool takes two type parameters instead of one, and the scripted and parallel TransitionConfig variants are commented out of the union.
Two runtime behaviors change with no code to port. Every LLM invocation is size-checked: a call past 200 000 bytes of text or 1 000 000 bytes of decoded image content fails with 413 and a PayloadTooLargeError. An image_url block whose URL isn't a data: URL is rejected.
If you adopted the Compass module, its source is a copy in your project under apps/storefront-middleware/sf-modules/compass/. At 2.1.3 that copy had eleven files calling createGenrativeUiComponent and one, middleware/tools/custom/searchProducts.ts, calling defineDynamicTool. Unless you've already renamed them yourself, bumping @alokai/compass to 4.0.0 fails your typecheck before it fails anything else.
The rework reaches your static tools as well as your dynamic ones. The callback moved out of the config object in both, so a defineTool call site needs the same port as a defineDynamicTool one. While you're in your code, check five more things: the startingPrompt rename, that your analytics action keeps none of model, modelConfiguration, and toolkit, the new callback signature, the payload limits that throw instead of trimming, and the rejection of remote images.
defineMocker records and replays outgoing HTTP inside the middleware
@alokai/connectThe middleware couldn't capture the HTTP calls an integration sends and play them back, so running an integration meant reaching a live platform.
defineMocker in @alokai/connect/integration-kit returns an extension and a programmatic handle. It intercepts the outgoing HTTP calls of your integrations, and it can record real traffic or replay saved recordings. When you register it, it also mounts five unauthenticated GET routes on your middleware: /mocker, /mocker/record, /mocker/replay, /mocker/stop, and /mocker/reset.
It's new - nothing at 2.1.3 exposed it - and it costs nothing until you register it. Before you wire it up, get two things right. The factory returns { api, extension }, not { extension, mocker }, so calling mocker.record() is a TypeError. enabled defaults to false in defineMocker, not to true as its JSDoc claims, so the extension does nothing until you pass enabled: true.
Keep it off outside development. GET /mocker returns every recording, bodies and headers included, and GET /mocker/replay lets anyone who can reach the middleware put it into replay mode.
The search-bloomreach installer targets the new page layout, and CMS modules rewrite your SAP Commerce Cloud route patterns
@vsf-enterprise/module-kitThe search-bloomreach installer targeted the old category and product page layout, and when it couldn't find a pattern it expected, it wrote a half-modified file and carried on.
The search-bloomreach installer targets the sf-modules/category and sf-modules/product layout this release introduces, and throws when a pattern it expects is missing, instead of writing a half-modified file.
In @vsf-enterprise/module-kit, baseCmsSchemas writes your Next.js app/[locale]/(cms)/[[...slug]]/page.tsx (or Nuxt pages/[...slug].vue) as a one-line re-export of the adopted module. On a sapcc or sapcc-b2b store, a schema:teardown hook rewrites category{/*slug} to c{/*slug} and product/*slug to p/*slug in sf-modules/<module>/config.ts. baseNextjsAppSchemas and baseNuxtPagesSchemas are removed from the package's exports.
If you adopt or re-adopt a CMS or search module after this release on a project that hasn't taken the /c/ and /p/ page layout, the install writes route patterns your app never requests. It also overwrites your existing CMS catch-all page instead of merging it, so copy that page aside first. If you author your own storefront module and it imports either removed schema, your module stops installing.
store deploy takes a custom image tag and stops building the framework you are not deploying
@alokai/cliThe deployed image tag was always the git commit SHA, so redeploying the same commit meant creating an empty commit.
store deploy gains --docker-image-tag, also readable from CLI_DOCKER_IMAGE_TAG, which replaces the auto-generated git commit SHA as the tag. The build, push, and trigger steps all use the same tag. Deployment composition skips the frontend app that doesn't match the store's deployment.framework, so a --framework nextjs deploy no longer runs nuxt prepare.
You can redeploy the same commit under a distinct tag, such as abc123-v2, instead of creating an empty commit. store deploy rejects an invalid tag up front, before the registry does. The composition change applies only to store deploy: store dev, store build, and store test still compose both apps, so your local loop doesn't change. Both changes arrive with @alokai/cli 2.5.0 and ask nothing of you.
All changes by area
Every change in 2.2 has one entry here, grouped by area. The highlights, from above and from the patch sections, are here too, marked with a star. Open an area to see its changes, and open a change to read why it changed and what changed. The upgrade guide from 2.1.3 to 2.2.6 collects the migration steps for the whole line.
Alokai ConnectAction needed11 changes
added defineMocker records and replays outgoing HTTP inside the middleware highlight
It costs nothing until you register it. The factory returns { api, extension }, not { extension, mocker }, so mocker.record() is a TypeError. enabled defaults to false despite its JSDoc, so the extension does nothing until you pass enabled: true. Keep it off outside development - GET /mocker returns every recording, bodies and headers included.
What changedWhy, and what changed ▸
The middleware couldn't capture the HTTP calls an integration sends and play them back, so running an integration meant reaching a live platform.
defineMocker in @alokai/connect/integration-kit returns an extension and a programmatic handle. It intercepts the outgoing HTTP calls of your integrations and can record or replay them. When you register it, it also mounts five unauthenticated GET routes on your middleware.
@alokai/connectfixed AppError stops leaking a bundler-mangled class name
If an error class of your own has a name that really ends in a digit, error.name reports it without the trailing digits.
What changedWhy, and what changed ▸
AppError strips trailing digits from the class name it reports in error.name. A bundler that renames HttpError to HttpError2 no longer leaks that name into error.name.
@alokai/connectchanged Three mocker dependencies install into every middleware
The three packages install into every middleware, whether or not you register the mocker.
What changedWhy, and what changed ▸
@alokai/connect lists nock, @mswjs/interceptors, and fast-json-stable-stringify as runtime dependencies. They back defineMocker.
@alokai/connectfixed Normalizer failures now name the normalizer, and keep their detail when something wraps them highlight 2.2.1
You can tell which normalizer failed and on what data, so you don't have to bisect through normalizers.override to find a platform-shape mismatch.
What changedWhy, and what changed ▸
A normalizer failure was a bare sentence that didn't say which normalizer ran or what it received. When another error carried it as its cause, even its Zod issues were lost.
A ValidationError from a normalizer now carries the input it received and the normalizer key it failed in. It keeps its original issues, and its message links to the guide on overriding normalizers. AppError.toJSON() serializes a nested AppError cause through that cause's own toJSON().
@alokai/connectchanged A normalizer validation error now returns your platform's raw payload to the caller highlight Action needed 2.2.1
If your storefront calls the middleware from the browser, as the shipped Next.js app does for cart, customer, and address operations, a normalizer mismatch sends your platform's own record of that entity to the client. It also lands in any client-side error telemetry that captures response bodies. A failing normalizer is usually systemic, so anyone who can trigger it can reproduce it. Strip the payload with a config-level errorHandler - if your middleware.config.ts already defines one, there's nothing to do.
What changedWhy, and what changed ▸
Normalizer validation errors now carry the platform input they failed on, and that error is the body the middleware sends back. So the input leaves the middleware too.
A ValidationError resolves to HTTP 400, and the built-in error handler sends error.toJSON() as the body for any status below 500. That body now includes data.input, the whole unnormalized platform object for the entity that failed.
@alokai/connectchanged Anything you have keyed on circuit breaker log lines stops matching highlight Action needed 2.2.5
A monitor matching Circuit breaker FAILURE goes quiet for good, and one that pages on warning starts firing on rejected requests it used to see at info. Re-point every monitor keyed on the old text or levels. The breaker behaves exactly as before.
What changedWhy, and what changed ▸
The old log lines said what the circuit breaker did, not what it meant for you. Two of them also logged at a severity that didn't match what happened.
Every circuit breaker log message has new text, and two change severity:
Circuit breaker FAILURE for <key>becomesCircuit breaker observed an upstream error for <key> - ...and drops fromerrortowarning.Circuit breaker REJECTED request for <key>becomesCircuit breaker blocked a request for <key> - ...and rises frominfotowarning.Circuit breaker OPENED for <key>becomesCircuit breaker OPEN for <key> - ....Circuit breaker HALF-OPENandCircuit breaker CLOSEDkeep their opening words and gain a tail, so a search for either still matches.Created circuit breaker for <key>becomesCircuit breaker created for <key> - ..., so a search for the old text no longer matches.
Each event's metadata also has an impact field (observing, blocking, probing, or recovered) and a recommendedAction field (investigate_upstream or none).
@alokai/connectfixed EXTREME_DEBUG in production really falls back to BALANCED now highlight Action needed 2.2.5
If your production middleware sets CB_PRESET=EXTREME_DEBUG or circuitBreaker.preset: 'EXTREME_DEBUG', its thresholds change on this upgrade. The error threshold moves from 95% to 50%, the per-call timeout from 120s to 20s, the volume threshold from 50 to 20, the reset timeout from 60s to 15s, and the rolling window from 120s to 20s. A flaky upstream that never tripped the breaker can now trip it, and a slow call that had two minutes now has twenty seconds. If you used the preset on purpose, set those thresholds one by one instead.
What changedWhy, and what changed ▸
In production, the middleware logged that it was falling back from EXTREME_DEBUG to BALANCED, and then kept EXTREME_DEBUG anyway.
When NODE_ENV is production, the EXTREME_DEBUG preset falls back to BALANCED, as the log always said it did.
@alokai/connectsecurity Every middleware log's metadata is redacted and size-capped highlight Action needed 2.2.5
A metadata key of your own that only contains one of those words, like tokenCount, refreshTokenExpiry, or apiKeyPrefix, now logs [REDACTED], and you can't opt out. A large payload you log on purpose comes back truncated. Check your own keys against the list and rename any that hold nothing sensitive, or you find out about a collision only when a log you need is empty.
What changedWhy, and what changed ▸
A vulnerability in what the middleware could write to your logs has been identified and fixed.
The middleware redacts the metadata of every log it writes, not only the circuit breaker's. A value becomes [REDACTED] when its key contains authorization, cookie, password, passwd, secret, token, credential, or apikey / api-key / api_key, in any case. The exact keys _header, _httpMessage, and outputData are redacted too.
Metadata is also capped in depth, string length, and array and object width. You can add keys to the list with redactKeys in the logger block of your middleware config, but you can't remove a built-in one.
@alokai/connectfixed Middleware failures now name what actually went wrong highlight 2.2.5
The two most common unclear middleware failures now say what they are on the first read. Client-facing bodies from AppError.toJSON() don't change. The generic 500 answer keeps the new, detailed message away from clients. There was no old leak to close.
What changedWhy, and what changed ▸
A broken extension export failed with TypeError: fnToExecute is not a function, which didn't say what broke. A transport error behind a normalized message never reached the log at all.
An API method that isn't a function now throws an AppError that names the extension and the function. The circuit breaker's failure log metadata includes the error's whole cause chain, so an ECONNRESET behind an HttpError reading Failed to communicate with external service is visible.
Errors of status 500 and above raised while the middleware prepares the API method are logged through the Alokai logger. The client gets the generic ServerError: Something went wrong. Please, check the logs for more details. Errors in the 4xx range keep their own message and aren't logged.
@alokai/connectadded Two more circuit breaker gauges, one of them per key 2.2.5
You can alert on the failure rate before the breaker opens.
What changedWhy, and what changed ▸
The middleware exports middleware_circuit_breaker_state_by_key (1 closed, 0.5 half-open, 0 open) and middleware_circuit_breaker_failure_rate (0-100 over the rolling window). Both are labelled by integration and key, next to the existing per-integration middleware_circuit_breaker_state. The failure rate updates on every counted upstream error, not only on a state change.
@alokai/connectchanged CircuitBreakerError carries a fixed message of its own 2.2.5
You see the new message only if a custom errorHandler forwards error.message, because the default error handler answers 500 and above with a generic body.
What changedWhy, and what changed ▸
CircuitBreakerError has a fixed message instead of the underlying library's text: Upstream temporarily unavailable for <key> - the circuit breaker is open and is failing requests fast to protect the upstream; retry shortly.
@alokai/connectStorefrontAction needed7 changes
changed The CMS mock's route maps and links moved to /c/ and /p/ Action needed
If your project still serves /category/, CMS content stops resolving on your category and product pages. The pages still load, and nothing in the logs says why.
What changedWhy, and what changed ▸
The CMS mock's route maps follow the new /c/ and /p/ URLs, but the page layout that serves those URLs doesn't reach an existing project. A project still on /category/ asks the mock for paths it no longer maps.
@vsf-enterprise/cms-mock-api 4.0.1 repoints its SAP Commerce Cloud and B2B route maps from /category{/*id} and /product/:id/:sku to /c{/*id} and /p/:id/:sku. Every mock banner and hero link moves from /category/... to /c/....
@vsf-enterprise/cms-mock-apichanged Alokai's injected scripts load later, and browser-side env() returns undefined highlight Action needed 2.2.2
In code that runs in the browser, env('NEXT_PUBLIC_...') returns undefined for as long as you're on this version, while server-side env() is unaffected. Your analytics also miss the first page view and any navigation during page load. Nothing warns you about either problem, and 2.2.3 reverts both strategies. If you call env() in code that runs in the browser, replace those calls.
What changedWhy, and what changed ▸
Alokai's two injected scripts loaded before the page became interactive, which put them on the critical path of every page load.
@vue-storefront/next and @alokai/instrumentation-next-component load their injected scripts with the afterInteractive strategy of next/script instead of beforeInteractive. The @vue-storefront/next script, alokaiEnv, sets window.__ALOKAI_ENV__, the only source env() reads in the browser. AlokaiProvider creates that script only during the server render, and Next.js adds an afterInteractive script from the client render, so on this version it never reaches the page.
@vue-storefront/next@alokai/instrumentation-next-componentperformance A leaner client bundle, but only if you take it into your own app highlight 2.2.2
The upgrade doesn't add any of this to an existing project, because the CLI wrote these files once when your project was created. Adopting the bundle trim takes three edits, two of them in a fixed order, and skipping it breaks nothing. The banner.tsx rewrite is optional, and you can copy it from a project generated at this release.
What changedWhy, and what changed ▸
Your client bundle carried the legacy polyfills for flatMap, Object.entries, and similar that Turbopack adds by default, and the generated config didn't turn them off.
A newly generated Next.js app ships apps/storefront-unified-nextjs/modern-polyfill.js, an effectively empty module. A turbopack.resolveAlias block in next.config.mjs points Next's polyfill-module at it. The generated components/cms/page/banner.tsx computes its image attributes once through getImageProps from next/image and requests quality: 45.
fixed Runtime NEXT_PUBLIC_* values reach the browser again highlight 2.2.3
Browser code that calls env('NEXT_PUBLIC_...') got undefined on every call on 2.2.2, and gets the real value again. Code that reads process.env.NEXT_PUBLIC_* directly was never affected. The fix works only if AlokaiProvider renders from your root layout, because Next.js supports beforeInteractive only there. If you worked around this on 2.2.2, you can stop - the upgrade step says which parts are worth undoing.
What changedWhy, and what changed ▸
On 2.2.2, browser code that called env('NEXT_PUBLIC_...') got undefined. 2.2.2 moved the alokaiEnv script to afterInteractive, and that move stopped the script from being injected at all.
The alokaiEnv script that AlokaiProvider renders loads with the beforeInteractive strategy of next/script again. Next.js puts a beforeInteractive script into the initial HTML and runs it before your own client code. The runtime values are in place before your first render.
@vue-storefront/nextfixed AlokaiInstrumentation stops losing the first page view highlight 2.2.3
On 2.2.2, the entry page view and any router navigation before hydration finished were never reported. Those events can't be recovered, so a dip over the time you spent on 2.2.2 is a measurement artifact, not lost traffic. The component renders nothing when NODE_ENV is development or NEXT_PUBLIC_ALOKAI_IS_SELF_HOSTED is "true".
What changedWhy, and what changed ▸
On 2.2.2, the script that installs the analytics history trace loaded after hydration instead of before it, so it missed the first page view.
The history-trace script that AlokaiInstrumentation renders loads with beforeInteractive again instead of afterInteractive. The history trace is installed before hydration.
@alokai/instrumentation-next-component@vue-storefront/nextsecurity Next.js 16.2.6 carries the May 2026 security release, and the pin is yours to move highlight Action needed 2.2.3
Move the next pin in apps/storefront-unified-nextjs/package.json yourself. version upgrade rewrites only the four core pins in that file, so until you edit it you stay on 16.1.6. 16.2.6 is a Next.js minor, not a patch, so read Next.js's own 16.2 release notes too. @vue-storefront/next 7.0.2 peer-requires next at ^16.0.0 at both tags, so you can take them in either order.
What changedWhy, and what changed ▸
A vulnerability in Next.js was found and fixed in the May 2026 Next.js security release. Your storefront keeps running the old Next.js until the pin that selects it moves.
The generated storefront's apps/storefront-unified-nextjs/package.json pins next at 16.2.6 instead of 16.1.6.
changed The custom-queries guides lead with a gql-tagged query 2.2.6
This release doesn't change your apps/storefront-middleware/api/custom-methods/custom.ts, and that file doesn't follow the pattern, although the guide says it does. Adopting the pattern is a hand edit. On a project generated before 2.0.0, the graphql-tag import won't resolve until you declare it.
What changedWhy, and what changed ▸
The commercetools and Magento custom-queries guides lead with a custom API method that tags its query with gql from graphql-tag and starts the template literal with a /* GraphQL */ comment. With the GraphQL extension, your editor highlights the query's syntax. If you point the extension at your schema through a graphql.config.yaml at the project root, it also autocompletes against the schema.
The customQuery route keeps its plain string, with a leading #graphql comment, because a string passed to a method can't be tagged.
CLI & toolingAction needed8 changes
changed The search-bloomreach installer targets the new page layout, and CMS modules rewrite your SAP Commerce Cloud route patterns highlight Action needed
Adopting or re-adopting a CMS or search module on a project that hasn't taken the /c/ and /p/ layout writes route patterns your app never requests. It also overwrites your CMS catch-all page instead of merging it, so copy that page aside first. A module of your own that imports either removed schema stops installing.
What changedWhy, and what changed ▸
The search-bloomreach installer targeted the old category and product page layout, and when it couldn't find a pattern it expected, it wrote a half-modified file and carried on.
The search-bloomreach installer targets the new sf-modules/category and sf-modules/product layout, and throws when a pattern it expects is missing. baseCmsSchemas writes your CMS catch-all page as a one-line re-export of the adopted module. On a SAP Commerce Cloud or SAP Commerce Cloud B2B store, a teardown hook rewrites the category and product route patterns. baseNextjsAppSchemas and baseNuxtPagesSchemas are removed from the package's exports.
@vsf-enterprise/module-kitadded store deploy takes a custom image tag and stops building the framework you are not deploying highlight
You can redeploy the same commit under a distinct tag, and store deploy rejects an invalid tag before the registry sees it. The composition change applies only to store deploy, so your local loop doesn't change.
What changedWhy, and what changed ▸
The deployed image tag was always the git commit SHA, so redeploying the same commit meant creating an empty commit.
store deploy gains --docker-image-tag, also readable from CLI_DOCKER_IMAGE_TAG. The build, push, and trigger steps all use the same tag. Deployment composition skips the frontend app that doesn't match the store's deployment.framework.
@alokai/clichanged vitest moves to 4.0.18 in the generated middleware
Not every project declares vitest in its middleware, so check yours first. A project generated before the middleware had a test setup doesn't declare it, and the upgrade doesn't add it, so there's nothing to move. If your own manifest declares it, the upgrade leaves it alone, and you bump it by hand.
What changedWhy, and what changed ▸
vitest moves from 2.1.9 to 4.0.18 in a freshly generated apps/storefront-middleware/package.json.
fixed A global CLI install no longer fails with MODULE_NOT_FOUND
The fix arrives with the package updates, and you have nothing to change.
What changedWhy, and what changed ▸
All four packages move from tinyexec@^0.3.1 to ^1.0.2. That resolves the tinyexec version conflict that made a global CLI install fail with MODULE_NOT_FOUND.
@alokai/cli@alokai/cli-plugin-compass@vsf-enterprise/module-kit@vsf-enterprise/storefront-clifixed Switching away from the CMS mock no longer leaves stale imports behind
When you switch a project from the CMS mock to a real CMS provider, no imports that no longer resolve stay behind.
What changedWhy, and what changed ▸
The @vsf-enterprise/module-kit cleanup removes both the @/sf-modules/cms-mock and the @sf-modules-middleware/cms-mock import forms. It also removes the 'cms-mock': cmsMockConfig entry and the unified-cms-mock SDK re-export, even when they don't match the generated text exactly.
@vsf-enterprise/module-kitremoved cms-mock can no longer be re-adopted as a module
Every generated project still ships the CMS mock by default. What's gone is adding it back as a module after you switched away from it.
What changedWhy, and what changed ▸
cms-mock is no longer in the CLI's module catalogue, so add-module cms-mock doesn't resolve to a module.
changed The dev-module authoring loop gets working tsconfig references and an env refresh
This reaches you only if you author storefront modules with dev-module. Then the module tsconfig files get working references, and you can refresh your .env files from the menu.
What changedWhy, and what changed ▸
In @vsf-enterprise/storefront-cli 3.3.0, the tsconfig files generated for Next and Nuxt module directories take their references from the tsconfig references of the .out apps.
The interactive menu gains a "Refresh .env files" option. It copies .env.example to .env and restarts the server. With --use-store-source-env, it uses the store's source .env.example instead of the one the install script modified.
@vsf-enterprise/storefront-clifixed modifyFile creates missing parent directories
module-kit can write a page file into a directory the module never had.
What changedWhy, and what changed ▸
modifyFile in @vsf-enterprise/file-modifier 4.0.1 creates missing parent directories for an outputPath instead of failing.
@vsf-enterprise/file-modifierCompassAction needed12 changes
changed Compass 4.0.0 rewrites how tools and workflows are declared highlight Action needed
If you adopted the Compass module, your copy of its source at 2.1.3 had eleven files calling createGenrativeUiComponent and one calling defineDynamicTool. Unless you've already renamed them yourself, bumping to 4.0.0 fails your typecheck first. The rework reaches your static tools as well as your dynamic ones.
What changedWhy, and what changed ▸
Compass had two factories, defineTool and defineDynamicTool, for one job, and both took a single object with the callback inside it.
defineDynamicTool is gone, and defineTool(config, callback) replaces both factories. It takes two arguments where 2.1.3 took one object. Workflow.startingPrompt is renamed workflowPrompt, and AnalyticsActionConfig collapses to { type: 'analytics' }.
createGenrativeUiComponent is renamed createGenerativeUiComponent. The scripted and parallel transition variants leave the union. Two runtime behaviors change with no code to port: every LLM invocation is size-checked, and an image_url that isn't a data: URL is rejected.
@alokai/compasschanged Compass moves from the GPT-4.1 models to GPT-5
Your assistant switches to GPT-5 with the package update, which changes what it costs per conversation and how it answers. If you take the new models, you have nothing to do. To stay on GPT-4.1 for now, set two environment variables.
step →What changedWhy, and what changed ▸
The heavy model slot moves from GPT-4.1 to GPT-5-chat. The medium slot moves from GPT-4.1-mini to GPT-5-mini. The light slot stays on gpt-4o-mini. The guided selling prompts are updated for GPT-5-chat.
@alokai/compassadded Periodic chat history compaction, tunable per workflow
A long conversation stops growing the prompt without limit. Nothing happens until you opt in.
What changedWhy, and what changed ▸
Compass can compact the chat history. It summarizes older messages and keeps the recent context. You opt in with useCompactHistory on the assistant hook, or pass it as an SDK parameter. The rolling summary is kept in the browser's localStorage.
You tune the compactor per workflow with compactor.summarizationThreshold ("small", "medium", or "large"), compactor.slidingWindow, and compactor.filterEntry.
@alokai/compassadded An image analysis action describes images in the chat history
An image a shopper sent earlier in the conversation stays in it as a description after compaction, instead of being dropped.
What changedWhy, and what changed ▸
An image analysis action describes the images in a conversation. The descriptions go into both the full and the compacted chat history.
@alokai/compassadded transformMessageContent filters each message before it reaches the LLM
You don't have to send a generative-UI payload to the model in full. The option's own docs warn you not to trim content the analytics workflow needs in full for sensitive-data redaction.
What changedWhy, and what changed ▸
transformMessageContent gives you the content of each non-system message, one at a time, before it reaches the LLM. You can trim it, for example a generative-UI payload, or return null to drop the message. A crash in the analytics action's own trimming of message content is fixed.
@alokai/compassadded Workflow payloads reach every tool, and shouldEnableTool keys off them
You can switch a tool off per request, based on the payload the workflow started with, instead of keeping it in the tool list every time.
What changedWhy, and what changed ▸
A workflow takes a payload through Workflow.payloadSchema. The payload reaches a tool's schema, callback, and cache.key as params.workflowPayload. It also reaches the new shouldEnableTool predicate the same way.
@alokai/compassadded State patches let a workflow drive the storefront's own state management
Your assistant can change what the page shows, not only what it says.
What changedWhy, and what changed ▸
A workflow can send patches to the storefront's own state management. The frontend half is the ai-controlled-state context in the Compass module.
@alokai/compassadded Responses carry token counts and model info
You can read each conversation's cost and model from the response, instead of inferring them.
What changedWhy, and what changed ▸
Streaming and non-streaming responses both carry API usage metadata: token counts and model info.
@alokai/compassadded Commerce Set-Cookie headers are streamed to the browser and applied there
A cookie the platform sets during an assistant call now reaches the browser, instead of stopping at the middleware. An HttpOnly one isn't set: the tool that sets it fails, and the assistant gets the error instead.
What changedWhy, and what changed ▸
Compass streams the Set-Cookie headers from commerce API responses to the browser. Compass's SDK sets them there with document.cookie, which can't set an HttpOnly cookie, so a tool that sets one during a streamed call throws an error naming the cookie.
@alokai/compasschanged Cart tools, cart resolution and a consolidated product search
Your assistant can remove several line items in one turn, and it stops guessing identifiers it was never given.
What changedWhy, and what changed ▸
The chatbot workflow gains a tool that removes several cart line items at once and a tool that clears the cart. getCart returns the cart itself, so the LLM can find line item IDs.
Product search is part of the main assistant action. The module's workflow no longer has a separate search-products router step, and the category enum strategy in the searchProducts tool schema changes to match. Compactor tool-call summaries are tighter, so the LLM stops inventing IDs and SKUs.
@alokai/compasschanged Workflows compile once, and a synchronous invoke returns every message Action needed
If your own chat frontend calls the synchronous invoke and reads a single message from the result, it now gets several, so update it to handle them.
What changedWhy, and what changed ▸
A synchronous invoke returned only the last message a workflow produced, not all of them. Each request also rebuilt the workflow from scratch.
Compass compiles a workflow once and reuses it across requests. A synchronous (non-streaming) invoke returns every message the workflow produced.
@alokai/compasschanged The Compass module's own source moved, and re-adopting it changes sendMessage and adds dependencies
The package update doesn't touch your sf-modules/compass/ copy, because a module's source is copied into your project when you adopt it. It stays yours until you re-adopt. If you re-adopt, update your own components that call sendMessage.
What changedWhy, and what changed ▸
Most of the Compass module's own source moved. The new source has a guided selling page with configure and search tabs, an AI canvas, and the ai-controlled-state context, which is the frontend half of state patches. The module declares three new frontend dependencies: @rjsf/core, @rjsf/utils, and @rjsf/validator-ajv8.
useAssistant's sendMessage changes from variadic sendMessage(...messages) to sendMessage(messages, options). It defaults useCompactHistory to true.
@alokai/cli-plugin-compass 1.1.3 re-pins zod to ^4.1.12.
@alokai/compass@alokai/cli-plugin-compassSAP Commerce CloudAction needed5 changes
added SAP Commerce Cloud storefronts serve /c/ and /p/ URLs
An existing project keeps its /category/ and /product/ URLs, because the CLI writes these files once, when it creates the project, and no upgrade rewrites them. To move to /c/ and /p/, copy the files by hand from a project generated at 2.2.0.
What changedWhy, and what changed ▸
SAP Commerce Cloud uses /c/ and /p/ for its category and product URLs, but the storefront served /category/ and /product/ instead.
Newly generated SAP Commerce Cloud and SAP Commerce Cloud B2B storefronts serve product listing pages under /c/... and product detail pages under /p/....
In the Next.js app, the app/[locale]/(default)/category and product route folders become c and p. The page logic moves into sf-modules/category/ and sf-modules/product/. config/routes.ts adds typed categoryPage/productPage builders, and config/navigation.ts swaps its patterns and gains a resolvePathname helper. Per-module json-ld.ts files replace helpers/generate-json-ld.ts.
A new config/rewrites.config.mjs, wired into next.config.mjs as rewrites, strips SAP's breadcrumb prefix, so /Collections/Streetwear/c/streetwear becomes /c/streetwear. The Nuxt app gets the equivalent under sf-modules/ plus utils/routes.ts.
added SAP Commerce customer login can use Authorization Code with PKCE Action needed
Nothing in this release sets pkceSupported for you, so the probe runs on every boot until you do. On an instance that doesn't answer promptly, it adds up to five seconds to startup. If your SAP OAuth client requires a registered redirect_uri, PKCE login fails until you also set OAuth.redirectUri.
What changedWhy, and what changed ▸
Customer login used only the resource-owner password flow (ROPC).
@vsf-enterprise/sapcc-api 11.1.0 adds OAuth Authorization Code with PKCE for customer login. At middleware init, it probes <OAuth.uri>/oauth/authorize with a 5 second timeout to detect PKCE support, and falls back to ROPC when the probe fails.
MiddlewareOAuthConfig gains an optional redirectUri. OAuth.pkceSupported forces one flow or the other and skips the probe.
@vsf-enterprise/sapcc-apichanged The PKCE support probe stops running through your axios interceptors Action needed 2.2.1
If your SAP traffic depends on an interceptor on the default axios export, you don't get an error - customer login quietly reverts to the password grant. Set OAuth.pkceSupported explicitly to skip the probe entirely, which is what 2.2.0's own step already asked for. If it's already set to true or false, the probe never runs, and if you register no interceptors on the default export, there's nothing to do.
What changedWhy, and what changed ▸
If an interceptor on the default axios export adds something your SAP traffic needs, such as a proxy agent, a client certificate, or a required header, the PKCE support probe no longer gets it, and you silently lose PKCE.
The PKCE support probe runs on a dedicated axios.create({ baseURL }) instance instead of the default axios export. That instance picks up axios.defaults but not the interceptors registered on the default export. The probe runs once at middleware start. On any failure, it falls back to ROPC and logs Failed to detect PKCE support. Falling back to ROPC. Set OAuth.pkceSupported explicitly to avoid startup auto-detection. at warning level.
@vsf-enterprise/sapcc-apifixed A rejected PKCE authorization redirect reports what it is 2.2.1
Nothing to do. If you match on the old message in your logs, it now has new wording.
What changedWhy, and what changed ▸
A PKCE authorization redirect that carries a bare ?error with no value counts as an authorization failure. The storefront still gets a 401, as before. The message changes from Failed to obtain authorization code from PKCE flow to PKCE authorization failed: unknown error.
@vsf-enterprise/sapcc-apifixed B2B post-login cart resolution asks SAP CC for the current org cart 2.2.2
After login, B2B shoppers get their organization's current cart instead of a stale or empty one. The fix arrives with the package bump.
What changedWhy, and what changed ▸
Right after login and a guest-cart merge, a B2B shopper could get a stale or wrong cart, or the storefront fell back to a fresh empty cart.
B2B post-login cart resolution calls SAP CC's getCurrentOrgCart endpoint instead of listing carts with getCarts. loginCustomer writes the org cart's code into the unified-cart-id cookie, so later requests target the right cart. B2B also gets its own getCart, built on the same lookup.
@vsf-enterprise/unified-api-sapcccommercetools1 change
fixed new SdkAuth() no longer throws under Node ESM 2.2.1
The bug hit anything that gets a commercetools token through tokenExtension or sdkHelpers, unless you pass your own sdkAuth in the integration configuration. The fix needs no configuration change.
What changedWhy, and what changed ▸
Under Node ESM, new SdkAuth() threw. @commercetools/sdk-auth is a CommonJS package, and a default import from ESM returned the module object instead of the constructor.
The integration resolves the CommonJS default export of @commercetools/sdk-auth before it calls it. @vsf-enterprise/commercetools-api declares "type": "module" at both 8.0.0 and 8.0.1.
@vsf-enterprise/commercetools-apiMagento 21 change
deprecated CustomQuery moves to @alokai/connect/middleware, with a peer pin that moves with it
Nothing breaks. You can switch your own imports to @alokai/connect/middleware when it suits you, because it has exported the type since before 2.1.3. Its CustomQuery<T> is the wider shape, { [P in T]?: string } & { metadata?: unknown }, so a value typed against the old Record<string, string> still fits. Because of the peer pin, you move those three package versions in one edit, not one at a time.
What changedWhy, and what changed ▸
@vsf-enterprise/magento-api 9.0.1 imports CustomQuery from @alokai/connect/middleware. The CustomQuery export in @vsf-enterprise/magento-types 4.0.3 is deprecated but still present. @vsf-enterprise/unified-api-magento 6.0.2 requires @vsf-enterprise/magento-types at exactly 4.0.3 and @vsf-enterprise/magento-api at ^9.0.1.
@vsf-enterprise/magento-api@vsf-enterprise/magento-typesContentstackAction needed2 changes
fixed Contentstack live preview tracks the draft you are actually editing Action needed 2.2.4
Your existing zero-argument callback still typechecks, so nothing breaks on upgrade. The SDK writes the new hash into the URL before it calls your callback, so a component that reads the query from window.location picks it up with no edit. The shipped Nuxt component does that, so Nuxt needs nothing here. On Next.js, router.refresh() doesn't observe that query, so your copy of live-preview.tsx needs the edit.
What changedWhy, and what changed ▸
When you edited a different draft, the storefront kept rendering the first one. initLivePreview read the live preview hash only once, when it initialized.
initLivePreview re-reads the Contentstack live preview hash on every edit and writes it into the vsf-live-preview-query search parameter. It passes the result, the current path plus that query, to your onLiveEdit callback as its first argument, livePreviewUrl. The callback type widens to (livePreviewUrl?: string) => Promise<void> | void.
The callback in the Contentstack CMS module's Next.js live-preview.tsx navigates with router.replace(livePreviewUrl) instead of router.refresh().
@vsf-enterprise/contentstack-sdkfixed Nested CMS fields get their data-cslp attributes back Action needed 2.2.4
getFieldAttributes(props, 'buttonA.label') returned {} before this, with no data-cslp attribute, so nested fields weren't clickable in Contentstack Visual Builder. Your copy of the module's Next.js render-cms-content.tsx needs the edit to get them back. The Nuxt RenderCmsContent.vue already preserved $ at 2.2.3 and isn't affected.
What changedWhy, and what changed ▸
Nested fields weren't clickable in Contentstack Visual Builder, while top-level fields were. The camelCase conversion turned the $ key that holds a nested prop's Visual Builder metadata into an empty string.
normalizeComponentProp in the module's Next.js render-cms-content.tsx takes the $ key out of a nested prop before the camelCase conversion and puts it back unchanged after it.
SmartEditAction needed4 changes
added SmartEdit resolves its homepage label on the root route Action needed
The default lives in the module template, not in the package, so a module you adopted before this release keeps your copy of the route map. Add the / entry to that copy yourself.
What changedWhy, and what changed ▸
Without a / entry in your route map, the root route served the fallback page instead of your SmartEdit homepage.
cms-smartedit maps / to pageLabelOrId: 'homepage' by default in resolvePages. SmartEdit's own "homepage" label resolves on the root route without configuration.
added SmartEdit gains getComponents and a hook for unknown component references
You can fetch a layout component that doesn't change across navigations without a full page request, and handle references the standard lookup can't resolve.
What changedWhy, and what changed ▸
@vsf-enterprise/smartedit-api 6.1.0 adds a getComponents method that fetches specific components by ID without a full page request. A new resolveUnknownComponents callback on UnifiedConfig handles references the standard component lookup doesn't return, such as components on a custom endpoint like /story-book/items. Both go through the same reference resolution and normalization as getPage.
The getDepthLevelForComponent callback also receives methodParams, the original arguments passed to getPage or getComponents. Depth can vary per page or per request.
@vsf-enterprise/smartedit-apifixed The SmartEdit SDK's bundled declarations name axios again
The upgrade brings the fix, and you have nothing to change.
What changedWhy, and what changed ▸
The bundled type declarations in @vsf-enterprise/smartedit-sdk 3.0.1 reference axios.AxiosResponse again. Earlier builds had undefined in its place.
@vsf-enterprise/smartedit-sdkfixed getProductReferences in the SmartEdit module normalizes to the catalog item type Action needed 2.2.2
The module's source is a copy in your project, so the fix is only in Alokai's copy, and no upgrade rewrites yours. If you adopted the module, repair your copy by hand.
step →What changedWhy, and what changed ▸
If you registered addCustomFields.normalizeProductCatalogItem - Compass does, with a required description: string - the SmartEdit module's product references failed typecheck with a $custom mismatch.
The SmartEdit CMS module's getProductReferences normalizes each reference's target with normalizeProductCatalogItem instead of normalizeProduct. That matches the SfProductCatalogItem type that ProductCardVertical expects.
Bloomreach DiscoveryAction needed2 changes
changed Bloomreach Discovery's middleware configuration is restructured Action needed
None of the moves is optional. A value left in its old place fails your typecheck, and at runtime the middleware ignores it.
step →What changedWhy, and what changed ▸
You configured Bloomreach Discovery in two places: the resolvers in createUnifiedExtension({ config }), and the settings they resolve against in the middleware configuration.
8.0.0 moves resolveDomainKey, resolveViewId, and resolveTrackingParams out of createUnifiedExtension({ config }) and into the middleware configuration block, beside discoveryApi. facetVersion, currencies, and the product field list move under discoveryApi.search, and facetFields is renamed to productFields. Each sub-API (search, autosuggest, bestseller, contentSearch, emailWidget, recommendationsPathways) takes an optional basePath, which replaces the removed discoveryApi.environment and top-level discoveryApi.basePath.
You can pass efq and userId per request through searchProducts params. You can set efq, statsField, and currencies globally under discoveryApi.search.
A normalizeFacet override and addCustomFields apply to legacy text, range, and category facets, not only to v3 ones. Category and price facets are no longer dropped under facetVersion: "3.0". Legacy category fields are detected by value shape rather than by field name.
NormalizeFacetInput, AddCustomFields, and InferAddCustomFields are exported for overrides and module augmentation. getProductDetails reads productFields from discoveryApi.search, so a custom catalog attribute such as price_range reaches addCustomFields.
@vsf-enterprise/bloomreach-discovery-apichanged The search-bloomreach installer writes Bloomreach's staging host Action needed
If you adopt the search module after this release, change the staging basePath before you deploy.
What changedWhy, and what changed ▸
A project that adopts the module after this release sends its production search to Bloomreach staging.
The search-bloomreach module's installer is rewritten. The SAP Commerce Cloud configuration block it injects carries basePath: 'https://staging-core.dxpapi.com/api/v1/core', which is Bloomreach's staging search host.
CI / deployment workflows2 changes
changed The generated CI setup action configures both private registries
The upgrade doesn't add these two lines to your existing workflow. If your committed root .npmrc maps both scopes, as a generated project's does, your CI keeps working without them. An .npmrc that's gitignored or edited changes that, so confirm it rather than assume: grep -n 'registry' .npmrc.
If it prints the @vsf-enterprise and @alokai registry lines, you have nothing to do. If it prints nothing, your CI resolves private packages some other way, and it's worth checking before your next deploy.
What changedWhy, and what changed ▸
The generated project's .github/actions/setup/action.yml runs npm config set "@vsf-enterprise:registry=..." and npm config set "@alokai:registry=..." after npm-cli-login. A checkout without the project's own .npmrc can then still resolve private packages.
changed The generated deployment workflow finds the Console API without a repository variable 2.2.2
A new project deploys without anyone setting a Console API URL. No upgrade rewrites your own workflow file, so it still reads the secret and then the variable.
What changedWhy, and what changed ▸
In a newly bootstrapped project, .github/workflows/continuous-delivery.yml still reads secrets.CONSOLE_API_URL first, and falls back to https://api.console.alokai.com instead of to vars.CONSOLE_API_URL, which it no longer reads.
Patches in 2.2.x
2.2.6
26 Jun 2026 · Patch · 2 of 82 packages moved
Nothing in this release reaches an existing project. Both packages that moved only changed version and are boilerplates the CLI fetches for you, so there's no manifest line to edit. The one change with content rewrites the commercetools and Magento custom-queries guides around a gql-tagged query in a custom API method. Your own apps/storefront-middleware/api/custom-methods/custom.ts doesn't change, and it holds no query at all.
One thing here can cost you time. The new guide says graphql-tag is "already available in the middleware". That's true of a project generated at 2.0.0 or later, and false of one generated before it and upgraded since.
2.2.5
25 Jun 2026 · Patch · 5 of 82 packages moved
This release is one @alokai/connect patch about how the middleware reports failures. Every circuit breaker log line has new text and two change severity, so anything keyed on the old text or levels stops matching. Every log's metadata passes through a redactor and a size cap that you can't switch off. The EXTREME_DEBUG circuit breaker preset said it fell back to BALANCED in production but didn't, and now it does, which changes when the breaker opens on any production project that set it.
Nothing here breaks a build. The four packages that moved without an entry of their own only re-pin @alokai/connect, and one of them is a peer that a Compass project has to follow.
Anything you have keyed on circuit breaker log lines stops matching
@alokai/connectThe old log lines said what the circuit breaker did, not what it meant for you. Two of them also logged at a severity that didn't match what happened.
Every circuit breaker log message has new text, and two change severity:
Circuit breaker FAILURE for <key>becomesCircuit breaker observed an upstream error for <key> - ...and drops fromerrortowarning.Circuit breaker REJECTED request for <key>becomesCircuit breaker blocked a request for <key> - ...and rises frominfotowarning.Circuit breaker OPENED for <key>becomesCircuit breaker OPEN for <key> - ....Circuit breaker HALF-OPEN for <key>andCircuit breaker CLOSED for <key>keep their opening words and gain a tail, so a search for either still matches.Created circuit breaker for <key>gains a tail and is reworded toCircuit breaker created for <key> - ..., so unlike those two, a search for the old text no longer matches.
Each event's metadata also has an impact field (observing, blocking, probing, or recovered) and a recommendedAction field (investigate_upstream or none).
A monitor matching Circuit breaker FAILURE goes quiet for good, and a monitor that pages on warning starts firing on rejected requests it used to see at info. Re-point every monitor keyed on the old text or levels. The breaker itself behaves exactly as before - only what it logs about itself changes.
EXTREME_DEBUG in production really falls back to BALANCED now
@alokai/connectIn production, the middleware logged that it was falling back from EXTREME_DEBUG to BALANCED, and then kept EXTREME_DEBUG anyway.
When NODE_ENV is production, the EXTREME_DEBUG preset falls back to BALANCED, as the log always said it did.
If your production middleware sets CB_PRESET=EXTREME_DEBUG or circuitBreaker.preset: 'EXTREME_DEBUG', its thresholds change on this upgrade. The error threshold moves from 95% to 50%, the per-call timeout from 120s to 20s, the volume threshold from 50 to 20, the reset timeout from 60s to 15s, and the rolling window the error rate is measured over from 120s to 20s. A flaky upstream that never tripped the breaker can now trip it, and a slow call that had two minutes now has twenty seconds. If you used the preset on purpose, set those thresholds one by one instead - the upgrade guide lists the five variables.
Every middleware log's metadata is redacted and size-capped
@alokai/connectA vulnerability in what the middleware could write to your logs has been identified and fixed.
The middleware redacts the metadata of every log it writes, not only the circuit breaker's. A value becomes [REDACTED] when its key matches /authorization/i, /cookie/i, /password/i, /passwd/i, /secret/i, /token/i, /credential/i, or /api[-_]?key/i. The exact keys _header, _httpMessage, and outputData are redacted too.
Metadata is also capped: depth 6, strings 4096 characters, arrays 50 items, and objects 100 keys. Circular references become [Circular]. You can add keys to the list with the new redactKeys option in the logger block of your middleware config, but you can't remove a built-in one.
A metadata key of your own that only contains one of those words, like tokenCount, refreshTokenExpiry, or apiKeyPrefix, now logs [REDACTED], and you can't opt out. A large payload you log on purpose comes back truncated. Check your own keys against the list and rename any that hold nothing sensitive, or you find out about a collision only when a log you need is empty.
Middleware failures now name what actually went wrong
@alokai/connectA broken extension export failed with TypeError: fnToExecute is not a function, which didn't say what broke. A transport error behind a normalized message never reached the log at all.
An API method that isn't a function now throws an AppError that names the extension and the function, instead of TypeError: fnToExecute is not a function. The circuit breaker's failure log metadata includes the error's whole cause chain, so an ECONNRESET behind an HttpError reading Failed to communicate with external service is visible.
Errors of status 500 and above raised while the middleware prepares the API method are logged through the Alokai logger. The client gets the generic ServerError: Something went wrong. Please, check the logs for more details. Errors in the 4xx range keep their own message and aren't logged, so an unknown function name is still a plain 404.
A broken extension export and a transport error behind a normalized message now say what they are on the first read. Client-facing bodies from AppError.toJSON() don't change. The generic 500 answer keeps the new, detailed message away from clients. There was no old leak to close. At 2.2.4, the only error this path produced was a 404, so nothing that already reached a client needs repairing.
2.2.4
16 Jun 2026 · Patch · 3 of 82 packages moved
This release fixes Contentstack live preview and nothing else. If you don't use Contentstack, nothing changes: freshly generated projects at 2.2.3 and 2.2.4 have byte-identical lockfiles and differ only in a version string. This release also moves none of the four packages version upgrade reaches, so to the CLI it's indistinguishable from 2.2.3.
If you use Contentstack, what you need to do depends on your framework. On Nuxt, the @vsf-enterprise/contentstack-sdk bump is the whole release, because the shipped Nuxt component already reads the live preview query from window.location, where this release writes the current draft's hash before it calls your callback. On Next.js, the bump gives you one of the three fixes. The other two are in module components your project holds its own copy of, and no upgrade rewrites them.
2.2.3
20 May 2026 · Patch · 5 of 82 packages moved
Two fixes in @vue-storefront/next undo the 2.2.2 regression: the script that publishes runtime NEXT_PUBLIC_* values to the browser and the script that installs the analytics history trace both load before hydration again. They failed differently on 2.2.2, which matters if you're on it. The env script was never injected at all, so browser-side env() always returned undefined, while the trace script loaded too late to catch the first page view.
The change you have to act on is neither of them. This release moves the generated storefront's next pin from 16.1.6 to 16.2.6 for the May 2026 Next.js security release, and that pin lives in a manifest the upgrade command can't touch.
Runtime NEXT_PUBLIC_* values reach the browser again
@vue-storefront/nextOn 2.2.2, browser code that called env('NEXT_PUBLIC_...') got undefined. 2.2.2 moved the alokaiEnv script to afterInteractive, and that move stopped the script from being injected at all.
The alokaiEnv script that AlokaiProvider renders loads with the beforeInteractive strategy of next/script again, instead of afterInteractive. Next.js puts a beforeInteractive script into the initial HTML and runs it before your own client code. The runtime values are in place before your first render.
On an upgraded project, env('NEXT_PUBLIC_...') returns the value at the client chunk's module scope, at the first client render, and inside an initial useEffect.
On 2.2.2, every call to env('NEXT_PUBLIC_...') from @vue-storefront/next in browser code returned undefined. That held for every render, effect, event handler, and client navigation, not only the first paint, and those calls get the real value again here. Code that reads process.env.NEXT_PUBLIC_* directly was never affected, because Next.js inlines those values at build time.
The values didn't arrive late - they never arrived, and document.getElementById('alokaiEnv') returned null. So if you're on 2.2.2, a call you expected to run late enough to be safe wasn't. In the SAP CDC module, for example, getCdcConfig() returned no API key, so the Gigya SDK never loaded.
You get the fix with the @vue-storefront/next bump, with one condition to check: Next.js supports beforeInteractive only in the root layout, so the fix works only if AlokaiProvider renders from yours. A generated project does that through components/providers.tsx, mounted from app/[locale]/layout.tsx, the file that holds <html> and <body>. If you've moved the provider into a nested layout or a page, move it back.
If you worked around this on 2.2.2, you can stop - the upgrade step says which parts are worth undoing.
See the step in the upgrade guide →AlokaiInstrumentation stops losing the first page view
@alokai/instrumentation-next-component@vue-storefront/nextOn 2.2.2, the script that installs the analytics history trace loaded after hydration instead of before it, so it missed the first page view.
The history-trace script that AlokaiInstrumentation renders loads with beforeInteractive again instead of afterInteractive. The history trace is installed before hydration.
On 2.2.2, the entry page view and any router navigation before hydration finished were never reported, so your analytics under-counted landings and early clicks. Those events were never sent and can't be recovered, so there's nothing to backfill. A dip in entry-page numbers over the time you spent on 2.2.2 is a measurement artifact, not lost traffic.
AlokaiInstrumentation renders inside AlokaiProvider, not from your own code, so the fix comes with the same @vue-storefront/next bump and there's nothing to mount. The component renders nothing when NODE_ENV is development or NEXT_PUBLIC_ALOKAI_IS_SELF_HOSTED is "true", so a self-hosted project sees no difference either way.
Next.js 16.2.6 carries the May 2026 security release, and the pin is yours to move
A vulnerability in Next.js was found and fixed in the May 2026 Next.js security release. Your storefront keeps running the old Next.js until the pin that selects it moves.
The generated storefront's apps/storefront-unified-nextjs/package.json pins next at 16.2.6 instead of 16.1.6.
Move the next pin in apps/storefront-unified-nextjs/package.json yourself. That manifest belongs to your project, and version upgrade rewrites only @alokai/cli, @alokai/connect, @vue-storefront/next, and @vue-storefront/nuxt in it, and only where the release moved them. That's why the @alokai/connect caret in the same file stays untouched in this release.
Running the upgrade doesn't move the next pin. Until you edit it yourself, you stay on 16.1.6, without whatever the May 2026 Next.js security release fixed. 16.2.6 is a Next.js minor, not a patch, so read Next.js's own 16.2 release notes too.
@vue-storefront/next 7.0.2 peer-requires next at ^16.0.0 at both tags, so neither version constrains the other and you can take them in either order.
2.2.2
20 May 2026 · Patch · 6 of 82 packages moved
This release's storefront change is meant to speed up page loads, but it breaks browser-side env(). The script that carries your NEXT_PUBLIC_* values into the browser never reaches the page, so env() returns undefined in browser code for as long as you're on this version. The same change stops AlokaiInstrumentation from reporting the first page view. 2.2.3 reverts both, so this is a version to pass through rather than stop on.
On SAP Commerce Cloud, B2B shoppers get their organization's current cart after login instead of one guessed from a cart list. If you adopted the SmartEdit CMS module, you have a typecheck break to repair in your own copy of the module's product-references code.
Alokai's injected scripts load later, and browser-side env() returns undefined
@vue-storefront/next@alokai/instrumentation-next-componentAlokai's two injected scripts loaded before the page became interactive, which put them on the critical path of every page load.
@vue-storefront/next and @alokai/instrumentation-next-component load their injected scripts with the afterInteractive strategy of next/script instead of beforeInteractive. In @vue-storefront/next, the script is alokaiEnv, which sets window.__ALOKAI_ENV__. That global is the only source env() reads in the browser. In @alokai/instrumentation-next-component, the script is alokaiInstrumentation.
AlokaiProvider creates alokaiEnv only during the server render. Next.js puts a beforeInteractive script into the HTML from the server render, but it adds an afterInteractive script from the client render, where alokaiEnv isn't created. So on this version, the script never reaches the page.
In code that runs in the browser, env('NEXT_PUBLIC_...') returns undefined for as long as you're on this version - on every render, effect, event handler, and client navigation, not only the first paint. In the browser, window.__ALOKAI_ENV__ stays undefined long after hydration, and document.getElementById('alokaiEnv') is null. Anything built on such a call gets nothing: for example, a CDC/Gigya-style integration that reads its API key never loads.
Server-side env() is unaffected, so your rendered HTML stays correct, and that makes the break easy to miss. The history-trace script also starts after hydration, so your analytics don't get the first page view or any navigation triggered during page load, and there's nothing you can configure to get them back on this version.
The upgrade reads as a performance change, and nothing warns you about either problem. 2.2.3 reverts both strategies. If you call env() in code that runs in the browser, replace those calls.
A leaner client bundle, but only if you take it into your own app
Your client bundle carried the legacy polyfills for flatMap, Object.entries, and similar that Turbopack adds by default, and the generated config didn't turn them off.
A newly generated Next.js app ships apps/storefront-unified-nextjs/modern-polyfill.js, an effectively empty module. A turbopack.resolveAlias block in next.config.mjs points Next's polyfill-module at it, which drops those polyfills from the client bundle. The generated components/cms/page/banner.tsx computes its image attributes once through getImageProps from next/image and requests quality: 45.
The upgrade doesn't add any of this to an existing project, because the CLI wrote these files once when your project was created. The bundle trim is worth adopting, and it takes three edits, two of them in a fixed order. Skipping it costs only bundle size and breaks nothing.
The banner.tsx rewrite is optional. You can copy it from a project generated at this release, and skipping it costs you only a slightly heavier banner image request.
2.2.1
17 Apr 2026 · Patch · 7 of 82 packages moved
The change to look at here is the one that reads smallest. A normalizer validation error now carries the platform object that failed the schema, and because that error resolves to a 400, the object goes out in the response body a browser client receives. The rest is narrower: new SdkAuth() no longer throws under Node ESM on commercetools, and two follow-ups land on the SAP Commerce PKCE login flow that 2.2.0 introduced. Watch one thing in the upgrade itself: version upgrade keeps whatever range prefix your manifest already had, so a caret on @alokai/connect survives it and resolves to a far later release than this one.
Normalizer failures now name the normalizer, and keep their detail when something wraps them
@alokai/connectWhen your platform's data didn't match the Unified Data Layer schema, the error was a bare sentence such as Missing required fields for customer. The Zod issues were attached, but nothing said which normalizer ran or what it received. When another error carried the validation error as its cause, even the issues were reduced to a name and a message.
A ValidationError thrown by a normalizer is re-thrown with two fields added to its data: input, the value the normalizer received, and normalizer, the key of the normalizer that failed. The original issues stay. The message links to https://docs.alokai.com/unified-data-layer/normalizers#overriding-normalizers.
AppError.toJSON() serializes a nested AppError cause through that cause's own toJSON(), instead of reducing it to message and name. A wrapped validation error keeps its issues.
You can tell which normalizer failed and on what data, and the message points you to the guide on overriding normalizers. You don't have to bisect through normalizers.override to find a platform-shape mismatch.
A normalizer validation error now returns your platform's raw payload to the caller
@alokai/connectNormalizer validation errors now carry the platform input they failed on, to make failures easier to diagnose. That error is also the body the middleware sends back, so the input leaves the middleware too.
A ValidationError resolves to HTTP 400. The built-in error handler sends error.toJSON() as the response body for any status below 500. From this release, that body includes data.input - the whole unnormalized platform object for the entity that failed - next to the Zod issues it already carried.
If your storefront calls the middleware from the browser, as the shipped Next.js app does for cart, customer, and address operations, a normalizer mismatch sends your platform's own record of that entity to the client. That record includes fields the Unified Data Model exists to keep on the server, and it lands in any client-side error telemetry that captures response bodies. A failing normalizer is usually systemic, not random, so anyone who can trigger it can reproduce it.
If your storefront calls the middleware from the browser, strip the payload at the middleware boundary with a config-level errorHandler. If your middleware.config.ts already defines one, there's nothing to do.