Vue Storefront is now Alokai! Learn More
Config Switcher

Config Switcher

The Config Switcher extension allows you to dynamically switch between different middleware configurations based on incoming requests. This is particularly useful for multi-store setups where you need to serve different configurations for different domains, stores, or clients.

Overview

In Alokai, the Frontend sends requests to the Middleware, which handles communication with various integrations (e.g., eCommerce platforms, CMS, etc.). The Config Switcher enables you to customize the configuration of these integrations on a per-request basis.

Imagine you're running an international retail business with multiple stores across different regions. Your Frontend code is identical for all stores - same components, same UI, same user experience. The only differences are:

  1. Each store has its own domain (us.yourstore.com, eu.yourstore.com, etc.)
  2. Each store connects to a different product catalog in your eCommerce platform
  3. Product prices and currencies differ by region

Without Config Switcher, you would need to deploy and maintain separate instances of your Middleware for each store, even though 90% of the code is identical. This creates unnecessary complexity and maintenance overhead.

Config Switcher solves this problem by allowing you to use a single Middleware instance that dynamically adjusts its configuration based on which store is making the request. Your Frontend code remains the same across all stores, and the Middleware automatically uses the right integration configuration based on the incoming request.

Installation

To use the Config Switcher, you need to add it as an extension to your middleware configuration. We recommend organizing your code by creating a separate file for the extension.

Prerequisites and compatibility

Before getting started with the installation, we recommend reviewing the Switch Strategies section to understand how the Config Switcher determines which configuration to use for each request. The example below uses SAPCC integration, but the same approach works for all other integrations (Commercetools, Magento, BigCommerce, etc.). The structure and implementation remain the same regardless of which integration you're using.

Configuration inheritance

Config Switcher automatically merges store-specific configurations with your base configuration. You only need to specify the options you want to override for each store - everything else will be inherited from the base configuration. This makes your configurations concise and easy to maintain.

  1. First, create a file for your Config Switcher extension:
// apps/storefront-middleware/integrations/sapcc/extensions/configSwitcher.ts
import { createConfigSwitcherExtension } from '@alokai/connect/config-switcher';
import type { MiddlewareConfig } from '@vsf-enterprise/sapcc-api';

export const configSwitcherExtension = createConfigSwitcherExtension<MiddlewareConfig>({
  // Config switcher options - specify only what's different for each store
  configuration: {
    'store-eu': {
      api: {
        baseSiteId: 'eu-electronics', // Override just the baseSiteId
        defaultCurrency: 'EUR', // and currency for EU store
      },
    },
    'store-us': {
      api: {
        baseSiteId: 'us-electronics', // Override just the baseSiteId
        defaultCurrency: 'USD', // and currency for US store
      },
    },
  },
});
  1. Then, import and use the extension in your integration configuration:
// apps/storefront-middleware/integrations/sapcc/config.ts
import { configSwitcherExtension } from './extensions/configSwitcher';

export default {
  integrations: {
    sapcc: {
      location: '@vsf-enterprise/sapcc-api/server',
      configuration: { 
        // Base configuration - shared settings for all stores
        api: {
          url: 'https://api.example.com',
          key: 'default-key',
          catalogId: 'electronics',      // Shared catalog ID
          catalogVersion: 'Online',      // Shared catalog version
          defaultLanguage: 'en'          // Default language for all stores
        }
      },
      extensions: (predefinedExtensions) => [
        ...predefinedExtensions,
        configSwitcherExtension,
      ]
    }
  }
};

Switch Strategies

The Config Switcher supports different strategies for determining which configuration to use for a given request. Let's continue our multi-store example to see how each strategy might be used:

Header Strategy (Default)

The header strategy uses the x-alokai-middleware-config-id header to determine which configuration to use. This is useful when you want explicit control over which store configuration to use, regardless of the domain.

For example, you might use this strategy during development and testing to easily switch between store configurations without changing domains:

createConfigSwitcherExtension({
  configuration: {
    'store-us': { /* US store config */ },
    'store-eu': { /* EU store config */ }
  },
  switchStrategy: 'header' // This is the default
})

Since 'header' is the default value, you can omit this parameter if you want to use the header strategy:

createConfigSwitcherExtension({
  configuration: {
    'store-us': { /* US store config */ },
    'store-eu': { /* EU store config */ }
  }
  // switchStrategy defaults to 'header'
})

Required header for default strategy

When using the header strategy, the Frontend MUST include the x-alokai-middleware-config-id header in EVERY request to the Middleware. If this header is missing, the request will fail with a 400 Bad Request error.

Setting up the Storefront Side

To use the header strategy, you need to configure your Storefront to send the x-alokai-middleware-config-id header with every request. Here's how to set it up:

  1. First, ensure you're using createAlokaiMiddleware in your Next.js middleware file:
// apps/storefront-unified-nextjs/middleware.ts
import { createAlokaiMiddleware } from '@vue-storefront/next';

export default createAlokaiMiddleware(async (request) => {
  // your middleware logic
});
  1. Then use defineGetConfigSwitcherHeader to calculate the config id
apps/storefront-unified-nextjs/sdk/modules/utils.ts
import { defineGetConfigSwitcherHeader } from '@vue-storefront/next';

const getConfigSwitcherHeader = defineGetConfigSwitcherHeader(({ pathname, searchParams, headers }) => {
  // Example: Use different configurations based on URL path
  return pathname?.startsWith('/electronics') ? 'electronics' : 'default';
});
  1. Pass the getConfigSwitcherHeader to all the SDK modules.
apps/storefront-unified-nextjs/sdk/modules/commerce.ts
import { defineSdkModule } from '@vue-storefront/next';
import { CommerceEndpoints } from 'storefront-middleware/types';

+import { getConfigSwitcherHeader } from './utils';

export const commerce = defineSdkModule(({ buildModule, config, getRequestHeaders, middlewareModule }) =>
  buildModule(middlewareModule<CommerceEndpoints>, {
    apiUrl: `${config.apiUrl}/commerce`,
    cdnCacheBustingId: config.cdnCacheBustingId,
    defaultRequestConfig: {
+     getConfigSwitcherHeader,
      headers: getRequestHeaders(),
    },
    ssrApiUrl: `${config.ssrApiUrl}/commerce`,
  }),
);

The getConfigSwitcherHeader function should return the configuration ID that matches one of the keys in your Config Switcher's configuration map. In this example:

  • If the URL path starts with /electronics, it uses the 'electronics' configuration
  • For all other paths, it uses the 'default' configuration

Domain Strategy

For our multi-store example, the domain strategy is often the most intuitive approach. It determines the configuration based on the domain of the request, allowing each of your store domains to automatically use the correct configuration:

createConfigSwitcherExtension({
  configuration: {
    'us.yourstore.com': {
      api: {
        baseSiteId: 'us-electronics',
        catalogId: 'us-catalog'
      }
    },
    'eu.yourstore.com': {
      api: {
        baseSiteId: 'eu-electronics',
        catalogId: 'eu-catalog'
      }
    }
  },
  switchStrategy: 'domain'
})

With this setup, when a customer visits us.yourstore.com, the Middleware automatically uses the US store configuration, connecting to the US catalog. When another customer visits eu.yourstore.com, the same Middleware instance connects to the EU catalog instead.

The domain strategy tries to extract the domain from:

  1. The origin header (for client-to-server communication)
  2. The x-forwarded-host header (for server-to-server communication)
  3. The host header (as a fallback)

Custom Strategy

For more complex scenarios, you might need a custom strategy. For instance, if your stores are differentiated by URL path rather than domain (e.g., yourstore.com/us, yourstore.com/eu):

createConfigSwitcherExtension({
  configuration: {
    'us': {
      api: {
        baseSiteId: 'us-electronics',
        catalogId: 'us-catalog'
      }
    },
    'eu': {
      api: {
        baseSiteId: 'eu-electronics',
        catalogId: 'eu-catalog'
      }
    }
  },
  switchStrategy: {
    resolveConfigId: (req) => {
      // Extract store ID from URL path
      const path = req.originalUrl;
      if (path.startsWith('/us/')) return 'us';
      if (path.startsWith('/eu/')) return 'eu';
      return 'us'; // Default to US store
    }
  }
})

Configuration Types

Continuing our multi-store example, let's look at different ways to provide store configurations:

Static Configuration

For a small number of stores with relatively static configurations, you can provide a configuration map directly:

createConfigSwitcherExtension({
  configuration: {
    'us.yourstore.com': {
      api: {
        baseSiteId: 'us-electronics',
        catalogId: 'us-catalog',
        currency: 'USD'
      }
    },
    'eu.yourstore.com': {
      api: {
        baseSiteId: 'eu-electronics',
        catalogId: 'eu-catalog',
        currency: 'EUR'
      }
    },
    'uk.yourstore.com': {
      api: {
        baseSiteId: 'uk-electronics',
        catalogId: 'uk-catalog',
        currency: 'GBP'
      }
    }
  },
  switchStrategy: 'domain'
})

Dynamic Configuration

As your business grows to dozens or hundreds of stores, managing configurations in code becomes unwieldy. In this case, you can use an async function to fetch configurations from a database or API:

createConfigSwitcherExtension({
  configuration: async () => {
    // Fetch store configuration from your store management system
    const response = await fetch("https://store-api.yourcompany.com/store-configs");
    return response.json();
  },
  switchStrategy: 'domain'
})

This approach allows your business team to manage store configurations through an admin interface without requiring code changes or redeployments.

Caching

In our multi-store example, fetching configurations from an external source for every request would create unnecessary load. The Config Switcher caches configurations to optimize performance.

For instance, when a customer browses your US store, the configuration is fetched once and then cached for subsequent requests:

createConfigSwitcherExtension({
  configuration: async (configId) => {
    // This will only be called once within the cache TTL period
    const response = await fetch("https://store-api.yourcompany.com/store-configs/");
    return response.json();
  },
})

By default, configurations are cached for 10 seconds. You can adjust the cache TTL (time-to-live) by setting the cacheTTL parameter to a different value in seconds:

createConfigSwitcherExtension({
  configuration: async () => {
    const response = await fetch("https://store-api.yourcompany.com/store-configs/");
    return response.json();
  },
  cacheTTL: 60 // Cache for 60 seconds instead of the default 10 seconds
})

For production environments with relatively static store configurations, consider increasing the cacheTTL to reduce the number of external API calls. If your store configurations rarely change, a value of 3600 (1 hour) or even 86400 (24 hours) might be appropriate.

Configuration Merging

The Config Switcher merges the selected configuration with the base configuration using a deep merge strategy. This is particularly useful for our multi-store example, as it allows you to define common settings once and only override what's different for each store.

For example, let's say your base SAPCC configuration in middleware.config.ts looks like this:

// Base configuration for SAPCC integration
const sapccConfig = {
  configuration: {
    api: {
      baseSiteId: 'apparel-uk',
      catalogId: 'apparelProductCatalog',
      catalogVersion: 'Online',
      defaultCurrency: 'GBP',
      defaultLanguage: 'en',
      uri: process.env.SAPCC_API_URI,
    },
    OAuth: {
      clientId: process.env.SAPCC_OAUTH_CLIENT_ID,
      clientSecret: process.env.SAPCC_OAUTH_CLIENT_SECRET,
      tokenEndpoint: process.env.SAPCC_OAUTH_TOKEN_ENDPOINT,
      tokenRevokeEndpoint: process.env.SAPCC_OAUTH_TOKEN_REVOKE_ENDPOINT,
      uri: process.env.SAPCC_OAUTH_URI,
      cookieOptions: {
        // Default cookie options
      }
    }
  }
};

And your store-specific configuration for the US store is:

{
  api: {
    baseSiteId: 'apparel-us',
    defaultCurrency: 'USD',
    // Only override what's different for the US store
  }
}

The resulting configuration for the US store will be:

{
  api: {
    baseSiteId: 'apparel-us',        // From store configuration
    catalogId: 'apparelProductCatalog', // From base configuration
    catalogVersion: 'Online',         // From base configuration
    defaultCurrency: 'USD',           // From store configuration
    defaultLanguage: 'en',            // From base configuration
    uri: process.env.SAPCC_API_URI,   // From base configuration
  },
  OAuth: {
    clientId: process.env.SAPCC_OAUTH_CLIENT_ID,         // From base configuration
    clientSecret: process.env.SAPCC_OAUTH_CLIENT_SECRET, // From base configuration
    tokenEndpoint: process.env.SAPCC_OAUTH_TOKEN_ENDPOINT, // From base configuration
    tokenRevokeEndpoint: process.env.SAPCC_OAUTH_TOKEN_REVOKE_ENDPOINT, // From base configuration
    uri: process.env.SAPCC_OAUTH_URI, // From base configuration
    cookieOptions: {
      // Default cookie options from base configuration
    }
  }
}

This approach is powerful because:

  1. You only need to specify the differences for each store
  2. Common settings are maintained in one place
  3. If you update a common setting in the base configuration, all stores inherit the change
  4. Store-specific overrides are clearly visible and easy to maintain

For example, if you need to add a new store for Germany, you might only need to specify:

{
  api: {
    baseSiteId: 'apparel-de',
    defaultCurrency: 'EUR',
    defaultLanguage: 'de'
  }
}

Everything else will be inherited from the base configuration, making your configurations concise and maintainable.

Error Handling

  • If using the header strategy and the x-alokai-middleware-config-id header is missing, the request will fail with a 400 Bad Request error.
  • If the specified config ID doesn't exist in your configuration map, the base configuration will be used.
  • For async configuration functions, if the function returns null or undefined for a given config ID, the base configuration will be used.