Alokai
Advanced

Stores

Scope carts and orders to a commercetools Store using the vsf-store cookie, and override the store per request.

A commercetools Store represents one selling unit inside a project - most often a market or a country, while the project itself represents the brand. Scoping cart and order entities to a Store lets you keep each market's data separate and grant API clients fine-grained, per-market access.

You don't have to swap your API calls to the store-scoped endpoints by hand. The integration resolves a store key on the middleware side and passes it to the commercetools mutations that accept one, so cart creation and checkout are scoped for you. This page explains where that key comes from, which methods use it, and how to override it per request.

How the store key is resolved

On every request the middleware resolves a store value from two sources, in order:

  1. The vsf-store cookie sent with the request.
  2. The store property in your integration configuration, used when the cookie is absent.

The resolved value is then reduced to a store key: everything before the first / is taken, so store-fr and store-fr/anything both resolve to the key store-fr. If neither source provides a value, no storeKey variable is sent at all and commercetools falls back to project scope.

This mirrors how vsf-country, vsf-currency, and vsf-locale work for their respective properties.

export const config = {
  configuration: {
    // ...
    // Default store, used when the request carries no store cookie.
    store: 'store-fr',
    // Optional - read the store key from a cookie of your own name.
    cookies: {
      storeCookieName: 'my-store',
    },
  },
};

The cookie names are nested

storeCookieName belongs inside the cookies object, alongside localeCookieName, currencyCookieName, countryCookieName, and channelCookieName. A storeCookieName placed directly on configuration is ignored and the default vsf-store stays in effect. See Configuration.

Nothing in the storefront sets vsf-store for you - unlike currency and locale, there is no built-in store switcher. Setting the cookie is your application's job, as shown in Setting the store per country.

What is store-scoped

The resolved store key reaches commercetools through three methods:

Methodcommercetools operation
createCartcreateMyCart(draft: $draft, storeKey: $storeKey)
updateCartupdateMyCart(id: $id, version: $version, actions: $actions, storeKey: $storeKey)
createMyOrderFromCartcreateMyOrderFromCart(draft: $draft, storeKey: $storeKey)

Every cart mutation except deleteCart delegates to updateCart, so it inherits the scoping without doing anything itself: addToCart, removeFromCart, updateCartQuantity, updateCartItemChannel, applyCartCoupon, and removeCartCoupon.

That covers the whole cart lifecycle in practice. If you use the Unified Data Layer, it is store-scoped with no changes on your side - getCart reads the active cart and falls back to createCart, and addCartLineItem and its siblings go through addToCart.

What is not store-scoped

Some methods are deliberately or incidentally outside this mechanism, and it's worth knowing which:

  • Cart retrieval. getMe runs me { activeCart }, which takes no storeKey variable. It isn't scoped by the cookie at all - it returns whatever cart the current commercetools session is already bound to. See How retrieval stays in the right store.
  • deleteCart. Depends on your @vsf-enterprise/commercetools-api version. From 11.0.1 the resolved key is forwarded to deleteMyCart, so deletion is scoped like the rest of the cart lifecycle. Before 11.0.1 its default query neither declares nor forwards a storeKey and deletion runs in project scope - pass the key explicitly with a custom query if you need it scoped.
  • Product, category, and customer methods. Their queries accept $storeKey per the commercetools schema, but the integration doesn't populate it. Product data in commercetools is project-level; scope it with a custom query if your project models it per store.

How retrieval stays in the right store

Creation is scoped by the store key. Retrieval is scoped by the session.

When createCart runs, commercetools creates the cart inside the given store and binds it to the access token behind the vsf-commercetools-token cookie - an anonymous session for guests, or the customer's session once they sign in. From then on me { activeCart } returns that cart because the token points at it, not because a store key is sent with the query.

The practical consequence: the store is fixed for the cart's lifetime, at the moment the cart is created.

Changing the cookie mid-session does not move an existing cart

Updating vsf-store changes the store that the next cart is created in. It does not move the cart the current session already holds, and me { activeCart } will keep returning the cart from the previous store. If a visitor can switch market mid-session, decide explicitly what should happen to their cart - carry it over by recreating it in the new store, or clear the commercetools session so a fresh cart is created.

Setting the store per country

Set vsf-store wherever your application already switches country or locale, using the store key that matches the visitor's market. Every subsequent cart call is scoped to that store with no other code changes.

'use client';

import { setCookie } from 'cookies-next';

const STORE_COOKIE = 'vsf-store';

export function useStoreSettings() {
  const setCurrentStore = (storeKey: string) => {
    setCookie(STORE_COOKIE, storeKey, {
      sameSite: 'strict',
      secure: true,
    });
  };

  return { setCurrentStore };
}

The cookie has to reach the middleware, so set it on a domain that middleware requests share. Leave httpOnly off when client-side code needs to write it - httpOnly cookies are invisible to JavaScript.

If each market has its own domain, prefer setting the cookie server-side during the request that resolves the domain, or set a per-domain default with the store configuration property and skip the cookie altogether.

Cookie-driven values are not cacheable

Any value the middleware reads from a cookie makes the response vary per visitor, which prevents CDN caching. This is fine for cart calls - they're personal anyway - but it's a reason to avoid store-scoping read-heavy product queries via the cookie.

Listing the available stores

Use getStores to fetch the stores configured in your commercetools project - useful for building a market switcher, or for validating a store key before you set the cookie.

const { stores } = await sdk.ct.getStores();

// stores.results -> [{ id, key, name, languages, distributionChannels, ... }]

You can filter by key or ID and paginate the result:

const { stores } = await sdk.ct.getStores({
  key: 'store-fr',
});

const paginated = await sdk.ct.getStores({
  limit: 10,
  offset: 20,
});

Passing locale explicitly instead of letting it fall back to the vsf-locale cookie keeps the response cacheable:

const { stores } = await sdk.ct.getStores({
  locale: 'en',
});

Required scope

Reading stores needs the view_stores:<PROJECT_KEY> scope on your API client. It's part of the client setup in Configuration.

Forcing a store per request

When you need a specific store regardless of the cookie, override the query variable with a custom query and pass the key through its metadata.

Define the custom query

export const config = {
  // ...
  customQueries: {
    'create-cart-custom-query': ({ query, variables, metadata }) => {
      variables.storeKey = metadata.storeKey;

      return {
        variables,
        query: `#graphql
          mutation createCart($draft: MyCartDraft!, $storeKey: KeyReferenceInput) {
            cart: createMyCart(draft: $draft, storeKey: $storeKey) {
              ${metadata.fields}
            }
          }
        `,
      };
    },
  },
};

Call the method with the store key

const { cart } = await sdk.ct.createCart(null, {
  createCart: 'create-cart-custom-query',
  metadata: { storeKey: 'store-fr', fields: 'id,version,store{key}' },
});

The same pattern applies to updateCart and createMyOrderFromCart, and it's how you scope a method the integration leaves in project scope - including deleteCart before @vsf-enterprise/commercetools-api 11.0.1.

Keep $storeKey in your own custom queries

Every default query that supports store scoping declares $storeKey and forwards it to the commercetools operation. If you replace one of those queries with a custom query and drop the variable, the operation silently runs in project scope - no error, just data from the wrong scope.

export const config = {
  // ...
  customQueries: {
    'update-cart-custom-query': ({ query, variables, metadata }) => ({
      variables,
      query: `#graphql
        mutation updateCart(
          $id: String
          $version: Long!
          $actions: [MyCartUpdateAction!]!
          $storeKey: KeyReferenceInput
        ) {
          cart: updateMyCart(id: $id, version: $version, actions: $actions, storeKey: $storeKey) {
            ${metadata.fields}
          }
        }
      `,
    }),
  },
};

You don't need to set variables.storeKey yourself here - the integration already resolved it and put it in variables. You only need to keep the variable declared and forwarded. Set it explicitly only when you want to override the resolved store.

See the queries you're replacing

Commercetools API GraphQL Queries lists the default query every method runs, so you can copy the exact signature - $storeKey included - before you change it.

On this page