Project Configuration

Storefront Next uses a centralized, type-safe configuration system that gives you:

  • IDE autocomplete: Full TypeScript support with suggestions as you type.
  • Environment overrides: Customize settings per environment without code changes.
  • Validation: Catch configuration errors at startup, not at run time.
  • Security by design: Clear separation between public and server-only values.

All configuration lives in a single file (config.server.ts) with defaults that you can override by using environment variables.

Quick Start

Storefront Next provides two APIs for accessing configuration.

For Loaders, Actions, Utilities: getConfig()

import { getConfig } from "@salesforce/storefront-next-runtime/config";

// Server loader/action - pass context
export function loader({ context }: LoaderFunctionArgs) {
  const config = getConfig(context);
  return { limit: config.search.products.hits.limit };
}

// Client loader - no context needed
export function clientLoader() {
  const config = getConfig();
  return { limit: config.search.products.hits.limit };
}

For React Components: useConfig()

import { useConfig } from "@salesforce/storefront-next-runtime/config";

function ProductGrid() {
  const config = useConfig();
  return <div>Showing {config.search.products.hits.limit} products</div>;
}

note: getConfig() returns the full AppConfig when called with context on the server. useConfig() returns a narrowed Omit<AppConfig, 'serverExtension'> so client-side reads can't reach server-only namespaces. This type narrowing happens automatically through the template's configuration in src/types/config.ts.

Configuration File

The main configuration file is config.server.ts in your project root.

// config.server.ts
import { defineConfig } from "@salesforce/storefront-next-runtime/config";
import type { Config } from "./src/types/config";

export default defineConfig<Config>({
  metadata: {
    projectName: "My Storefront",
    projectSlug: "my-storefront",
  },
  runtime: {
    ssrOnly: [],
    ssrParameters: { ssrFunctionNodeVersion: "24.x" },
  },
  app: {
    commerce: {
      /* Commerce API settings */
    },
    defaultSiteId: "RefArch",
    /* Locale, currency, features */
    pages: {
      /* Page-specific settings */
    },
    global: {
      /* Branding, product listing defaults */
    },
    images: {
      /* Image optimization */
    },
    performance: {
      /* Caching */
    },
    engagement: {
      /* Analytics adapters */
    },
  },
});

For detailed descriptions of all configuration options, see the Configuration Options Reference section below.

The configuration is organized into three main sections.

Section Purpose Client Access
metadata Project name and slug for deployment Server only
runtime SSR and deployment settings Server only
app Application settings (commerce, features, UI) Server and client

The app section is automatically made available to client-side code. The metadata and runtime sections remain server-only.

The runtime.ssrOnly option accepts an array of glob patterns for files that must be available on the server but aren't publicly accessible from the client. Use this option for server-side resources that aren't exposed via public URLs.

warning: Don't import config.server.ts directly in your app code. The .server.ts suffix prevents the file from being bundled into client-side code (a React Router framework feature). Always use getConfig() or useConfig() to access configuration values. These APIs ensure that the configuration is properly loaded and available in the correct context.

How Configuration Works

The configuration system follows this flow:

  1. Types defined in src/types/config.ts — AppConfig defines all app fields, Config = BaseConfig<AppConfig>
  2. Defaults defined in config.server.ts — clean, with no process.env references
  3. Environment variables with PUBLIC__ prefix are automatically merged by defineConfig() — this happens at server startup
  4. Final config is made available via:
    • getConfig(context) for server loaders/actions (returns full AppConfig)
    • getConfig() for client loaders (returns narrowed config without server-only namespaces)
    • useConfig() for React components (returns narrowed config)
    • window.__APP_CONFIG__ for client code

The type narrowing is automatic because the template fills two augmentation slots in src/types/config.ts:

declare module "@salesforce/storefront-next-runtime/config" {
  interface AppConfigShape extends AppConfig {}
  interface ClientFacingAppConfigShape extends ClientAppConfig {}
}

ClientAppConfig is Omit<AppConfig, ServerOnlyNamespace>, which keeps server-only configuration off the client at both runtime and type level.

note: Multi-template caveat If you build two templates in the same TypeScript program (rare), only one extends per slot wins. Fall back to explicit per-call generics: getConfig<MyAppConfig>(context) and useConfig<MyClientAppConfig>().

Required vs Optional Variables

Every variable the storefront recognizes is listed in this guide. Set the Required variables in .env. Everything else has a working default in config.server.ts.

Required for the app to start

Copy .env.default to .env and set these required B2C Commerce credentials:

Variable Purpose
PUBLIC__app__commerce__api__clientId SLAS client ID provisioned in B2C Commerce
PUBLIC__app__commerce__api__organizationId B2C Commerce organization or realm ID (for example, f_ecom_zzrf_001)
PUBLIC__app__commerce__api__shortCode SCAPI short code for your tenant (for example, kv7kzm78)
PUBLIC__app__commerce__api__clientId=your-client-id
PUBLIC__app__commerce__api__organizationId=your-org-id
PUBLIC__app__commerce__api__shortCode=your-short-code

Required for pnpm push (Managed Runtime deploy)

Variable Default Notes
MRT_PROJECT falls back to package.json#name MRT project slug. Owned by the MRT/Fast Setup team.
MRT_TARGET — MRT deploy target (for example, development, production).

For more information, see Storefront Next CLI.

Server-only secrets (never prefix with PUBLIC__)

Variable Used by Notes
COMMERCE_API_SLAS_SECRET src/lib/api-clients.server.ts, e2e/src/utils/scapi-helper.ts Required only with private-client SCAPI auth.
GUEST_ORDER_LOOKUP_COOKIE_SECRET src/lib/order/session.server.ts Signs the guest order lookup state cookie. Required when guestOrderLookup.enabled is true. Falls back to CLIENT_SECRET if unset; if neither is set, the feature fails closed with a CONFIGURATION_ERROR.
MARKETING_CLOUD_CLIENT_ID, MARKETING_CLOUD_CLIENT_SECRET, MARKETING_CLOUD_AUTH_BASE_URL, MARKETING_CLOUD_REST_BASE_URL Passwordless login email delivery Required only when passwordlessLogin.mode = 'email' and you ship your own MC tenant.
SCAPI_PROXY_HOST vite-plugins/env-validation.ts, src/middlewares/app-config.server.ts Internal-developer-only override (workspace proxy).

warning: Don't add server-only secrets to config.server.ts and don't give them a PUBLIC__ prefix. Read them directly from process.env in server-side code, such as loaders, actions, and middleware. Anything with the PUBLIC__ prefix is bundled into the browser-visible JavaScript.

For SLAS private client setup in B2C Commerce, see Authorization for Shopper APIs.

Optional PUBLIC__* overrides (defaults in config.server.ts)

For a comprehensive list of optional environment variables with their defaults and effects, see the Optional Configuration Variables section below.

Optional non-PUBLIC__ runtime/deploy variables

Variable Default Effect
HYBRID_PROXY_ENABLED false Enable Vite hybrid proxy
HYBRID_ROUTING_RULES — Cloudflare-style routing expression for hybrid proxy
HYBRID_PROXY_LOCALE falls back to i18n.fallbackLng Locale for SFRA path transformation
SFCC_ORIGIN — SFCC origin URL (required when hybrid proxy enabled)
SFCC_LOG_LEVEL warn (prod) / info (dev) Log verbosity (error | warn | info | debug)

For feature-specific configuration, see the dedicated guides:

Environment Variable Overrides

Override any configuration value by using environment variables with the PUBLIC__ prefix. With environment variables, you can customize settings per environment (development, staging, production) without modifying code.

Understanding the Path Syntax

The double underscore (__) lets you navigate nested config paths. Think of it as replacing the dot (.) in JavaScript object notation:

# This environment variable:
PUBLIC__app__commerce__api__clientId=abc123

# Maps to this config path:
config.app.commerce.api.clientId

# Which creates this structure:
{
  app: {
    commerce: {
      api: {
        clientId: 'abc123'
      }
    }
  }
}

Value Types

Values are automatically parsed to the correct type:

PUBLIC__app__myFeature__count=42           # → number
PUBLIC__app__myFeature__enabled=true       # → boolean
PUBLIC__app__myFeature__items=["a","b"]    # → array
PUBLIC__app__myFeature__data='{"x":1}'     # → object
PUBLIC__app__myFeature__name=hello         # → string
PUBLIC__app__myFeature__value=             # → empty string

You can also set entire nested objects at once using JSON:

# Instead of setting each value separately:
PUBLIC__app__myFeature__option1=value1
PUBLIC__app__myFeature__option2=value2
PUBLIC__app__myFeature__nested__enabled=true

# Use a single JSON value:
PUBLIC__app__myFeature='{"option1":"value1","option2":"value2","nested":{"enabled":true}}'

Important Notes

Case doesn't matter: You can use any casing (lowercase, UPPERCASE, or MixedCase), and it will normalize to match your config.server.ts:

PUBLIC__app__commerce__api__clientId=abc123    # ✅ Works
PUBLIC__APP__COMMERCE__API__CLIENTID=abc123    # ✅ Also works
PUBLIC__App__Commerce__Api__ClientId=abc123    # ✅ Also works

Paths must exist in config: You can only override paths that are already defined in config.server.ts. This prevents typos from silently failing:

PUBLIC__app__site__local=en-GB  # ❌ Error: "local" doesn't exist (did you mean "locale"?)

More specific paths win: When paths overlap, deeper paths take precedence:

PUBLIC__app__myFeature='{"setting1":500,"setting2":1000}'
PUBLIC__app__myFeature__setting1=999  # ← This wins (more specific)
# Result: setting1=999, setting2=1000

Depth limit: Paths are limited to 10 levels deep. For deeper structures, use JSON values instead:

# ❌ Too deep (11 levels):
PUBLIC__a__b__c__d__e__f__g__h__i__j__k=value

# ✅ Use JSON instead:
PUBLIC__app__myFeature='{"deep":{"nested":{"structure":{"works":"fine"}}}}'

Security: PUBLIC__ vs Non-Prefixed

PUBLIC__ prefix → Exposed to the browser (bundled into client JavaScript)

  • ✅ Use for: Client IDs, site IDs, locales, feature flags, public API endpoints
  • ❌ Never use for: API secrets, passwords, private keys, authentication tokens

No prefix → Server-only (never exposed to client)

  • ✅ Use for: SLAS secrets, database credentials, private tokens
# ✅ Safe to expose to client:
PUBLIC__app__commerce__api__clientId=abc123

# ✅ Server-only secret (no PUBLIC__ prefix):
COMMERCE_API_SLAS_SECRET=your-secret-here

Read server-only secrets directly from process.env in your server code—never add them to config.

Merge Behavior

Environment variables are deep merged into defaults from config.server.ts:

// config.server.ts (defaults)
export default defineConfig({
  app: {
    myFeature: {
      debounce: 750,
      maxItems: 999,
      enabled: true,
    }
  }
});

// With env var:
// PUBLIC__app__myFeature__debounce=1000

// Final result:
{
  app: {
    myFeature: {
      debounce: 1000,        // ← overridden
      maxItems: 999,         // ← preserved
      enabled: true,         // ← preserved
    }
  }
}

Adding Configuration

1. Update the type (src/types/config.ts)

The template defines its own AppConfig type with all the fields it needs — SCAPI credentials, pages, features, and any custom domain fields. BaseConfig<AppConfig> wraps it with metadata and runtime sections:

import type { BaseConfig } from "@salesforce/storefront-next-runtime/config";
import type { Site, Url } from "@salesforce/storefront-next-runtime/config";

// Define all app fields in one flat type
export type AppConfig = {
  commerce: { api: { clientId: string /* ... */ }; sites: Array<Site> };
  defaultSiteId: string;
  url?: Url;
  myFeature: {
    enabled: boolean;
    maxItems: number;
  };
  // ...other template-specific fields (pages, features, global, etc.)
};

// Full config type used by config.server.ts
export type Config = BaseConfig<AppConfig>;

2. Add default value (config.server.ts)

import { defineConfig } from "@salesforce/storefront-next-runtime/config";
import type { Config } from "./src/types/config";

export default defineConfig<Config>({
  metadata: { projectName: "My Store", projectSlug: "my-store" },
  app: {
    // SCAPI fields
    commerce: { api: { clientId: "", organizationId: "", siteId: "", shortCode: "" }, sites: [] },
    defaultSiteId: "RefArch",
    // Template-specific fields
    myFeature: {
      enabled: false, // Just the default - no process.env needed!
      maxItems: 10,
    },
  },
});

3. Override via environment variables

# No code changes needed - just use the PUBLIC__ prefix!
PUBLIC__app__myFeature__enabled=true
PUBLIC__app__myFeature__maxItems=20

4. Use it in your code

In React components:

import { useConfig } from "@salesforce/storefront-next-runtime/config";

export function MyComponent() {
  const config = useConfig();

  if (config.myFeature.enabled) {
    const maxItems = config.myFeature.maxItems;
    // Your feature code here
  }
}

In loaders/actions:

import { getConfig } from "@salesforce/storefront-next-runtime/config";

export function loader({ context }: LoaderFunctionArgs) {
  const config = getConfig(context);

  if (config.myFeature.enabled) {
    // Your loader code here
  }
}

5. Add a new config value during app creation

In config-meta.json:

  • Add the name and key value to the config array
  • This will cause the create-storefront script to ask for user input, using the value in .env.default as default value
{
  "configs": [
    {
      "name": "SLAS Client ID",
      "key": "PUBLIC__app__commerce__api__clientId"
    },
    {
      "name": "Organization ID",
      "key": "PUBLIC__app__commerce__api__organizationId"
    },
    {
      "name": "Short Code",
      "key": "PUBLIC__app__commerce__api__shortCode"
    }
  ]
}

Security: Public vs. Private Configuration

The PUBLIC__ prefix indicates values that are safe to expose to the browser. These values are bundled into client-side JavaScript.

important: Don't use PUBLIC__ for secrets, API keys, passwords, or authentication tokens. These values are visible to anyone who uses your site.

Use PUBLIC__ For Use Non-Prefixed For
Client IDs API secrets
Site IDs Private keys
Locales and currencies Database credentials
Feature flags Authentication tokens
Public API endpoints SLAS secrets

For server-only secrets, use environment variables without the PUBLIC__ prefix and read them directly from process.env.

# .env - Server-only secret (no PUBLIC__ prefix)
COMMERCE_API_SLAS_SECRET=your-secret-here
// In server-side code only
const slasSecret = process.env.COMMERCE_API_SLAS_SECRET;

Commerce Sites Configuration

Site-level settings (default locale, default currency, supported locales/currencies, cookie domain) come from the commerce.sites config array. You can override it with the environment variable PUBLIC__app__commerce__sites, set to a JSON array.

# Example: one site with multiple locales and currencies (single line)
PUBLIC__app__commerce__sites='[{"cookies":{"domain":null},"id":"RefArchGlobal","defaultLocale":"en-GB","defaultCurrency":"USD","supportedLocales":[{"id":"en-GB","preferredCurrency":"USD"},{"id":"de-DE","preferredCurrency":"EUR"}],"supportedCurrencies":["EUR","USD"]}]'

Multi-line JSON is supported in .env files:

# Multi-line format (easier to read and edit)
PUBLIC__app__commerce__sites='[
  {
    "id": "RefArchGlobal",
    "defaultLocale": "en-GB",
    "defaultCurrency": "GBP",
    "cookies": {"domain": null},
    "supportedLocales": [
      {"id": "en-US", "preferredCurrency": "USD"},
      {"id": "de-DE", "preferredCurrency": "EUR"},
      {"id": "en-GB", "preferredCurrency": "GBP"}
    ],
    "supportedCurrencies": ["EUR", "GBP", "USD"]
  }
]'

Per-Site Cookie Domain Override

Each site can specify its own cookies.domain to override the global app.cookies.domain setting. This is useful when different sites need different cookie scoping:

PUBLIC__app__commerce__sites='[
  {
    "id": "SiteA",
    "cookies": {"domain": ".example.com"},
    ...
  },
  {
    "id": "SiteB",
    "cookies": {"domain": ".other-domain.com"},
    ...
  }
]'

For the full schema, all properties, and troubleshooting, see Configure Multisite URLs.

Accessing Configuration

In Loaders and Actions

Use getConfig() with the router context.

import { getConfig } from "@salesforce/storefront-next-runtime/config";

export function loader({ context }: LoaderFunctionArgs) {
  const config = getConfig(context);
  const { clientId, siteId } = config.commerce.api;
  return { siteId };
}

export async function action({ context, request }: ActionFunctionArgs) {
  const config = getConfig(context);
  // Use config values...
}

In React Components

Use the useConfig() hook:

import { useConfig } from "@salesforce/storefront-next-runtime/config";

function Header() {
  const config = useConfig();
  return <h1>{config.global.branding.name}</h1>;
}

In Client Loaders

Client loaders don't have access to router context. Call getConfig() without arguments.

export function clientLoader() {
  const config = getConfig(); // Uses window.__APP_CONFIG__
  return { limit: config.search.products.hits.limit };
}

In Custom Middleware

Custom middleware can read the resolved app config from appConfigContext:

import { appConfigContext } from "@salesforce/storefront-next-runtime/config";

const config = context.get(appConfigContext);

Adding New Configuration

To add a new configuration value, follow these steps.

1. Update the Type

The template defines its own AppConfig type with all the fields it needs. Update src/types/config.ts:

import type { BaseConfig } from "@salesforce/storefront-next-runtime/config";
import type { Site, Url } from "@salesforce/storefront-next-runtime/config";

// Define all app fields in one flat type
export type AppConfig = {
  commerce: { api: { clientId: string /* ... */ }; sites: Array<Site> };
  defaultSiteId: string;
  url?: Url;
  myFeature: {
    enabled: boolean;
    maxItems: number;
  };
  // ...other template-specific fields (pages, features, global, etc.)
};

// Full config type used by config.server.ts
export type Config = BaseConfig<AppConfig>;

2. Add Default Value

Add the default in config.server.ts:

import { defineConfig } from "@salesforce/storefront-next-runtime/config";
import type { Config } from "./src/types/config";

export default defineConfig<Config>({
  metadata: { projectName: "My Store", projectSlug: "my-store" },
  app: {
    // SCAPI fields
    commerce: { api: { clientId: "", organizationId: "", siteId: "", shortCode: "" }, sites: [] },
    defaultSiteId: "RefArch",
    // Template-specific fields
    myFeature: {
      enabled: false, // Just the default - no process.env needed!
      maxItems: 10,
    },
  },
});

3. Override via Environment Variables

No code changes needed. Use the PUBLIC__ prefix:

PUBLIC__app__myFeature__enabled=true
PUBLIC__app__myFeature__maxItems=20

4. Use in Your Code

In React components:

import { useConfig } from "@salesforce/storefront-next-runtime/config";

export function MyComponent() {
  const config = useConfig();

  if (config.myFeature.enabled) {
    const maxItems = config.myFeature.maxItems;
    // Your feature code here
  }
}

In loaders/actions:

import { getConfig } from "@salesforce/storefront-next-runtime/config";

export function loader({ context }: LoaderFunctionArgs) {
  const config = getConfig(context);

  if (config.myFeature.enabled) {
    // Your loader code here
  }
}

5. Add to config-meta.json (Optional)

If you want the create-storefront script to prompt for this value during app creation:

{
  "configs": [
    {
      "name": "SLAS Client ID",
      "key": "PUBLIC__app__commerce__api__clientId"
    },
    {
      "name": "My Feature Max Items",
      "key": "PUBLIC__app__myFeature__maxItems"
    }
  ]
}

This causes the create-storefront script to ask for user input, using the value in .env.default as the default value.

Extension Configuration

Extensions add configuration without editing src/types/config.ts or config.server.ts.

Extension Config Auto-Discovery

Drop a config.ts in your extension folder that default-exports a plain object. The build prestep (pnpm dev / pnpm build) discovers it, merges it into config.app.extension.<camelCaseFolder>, and derives the type automatically.

// src/extensions/my-feature/config.ts
export default {
  enabled: false,
  timeout: 5000,
};

Merchants override per environment with PUBLIC__app__extension__<key>__<setting>:

PUBLIC__app__extension__myFeature__enabled=true
PUBLIC__app__extension__myFeature__timeout=3000

No core-file edits needed. Extension keys are set via PUBLIC__ env vars / .env; they are not added to config-meta.json, so they don't appear in create-storefront prompts.

Server-Only Extension Config

config.ts reaches the browser by design. For values an extension needs at runtime that must never be serialized into window.__APP_CONFIG__ (vendor-side SCAPI service overrides, retry budgets, internal-only endpoints), drop a server-config.ts next to config.ts:

// src/extensions/loqate-address-verification/server-config.ts
export default {
  scapiOverride: "",
  retryBudget: 3,
};

Read the value from a server loader, action, or middleware:

import { getConfig } from "@salesforce/storefront-next-runtime/config";

export function loader({ context }: LoaderFunctionArgs) {
  const { scapiOverride } = getConfig(context).serverExtension?.loqateAddressVerification ?? {};
  // …
}

The build prestep aggregates every extension's server-config.ts into src/extensions/config/server.ts (auto-generated, do not edit) and merges it into config.app.serverExtension.<camelCaseFolder>.

Three structural guarantees keep the values off the client:

  1. The client config extractor (src/lib/app-config-client.ts) strips app.serverExtension before writing window.__APP_CONFIG__.
  2. A Vite plugin (vite-plugins/server-only-config-guard.ts) fails the build if any client chunk imports src/extensions/config/server.
  3. useConfig() and getConfig()'s client-facing overloads (no-arg and getConfig(ctx | undefined)) are type-narrowed to omit app.serverExtension, so reading .serverExtension from any of them is a TypeScript error in client code. The server getConfig(context) overload still returns the full shape.

There is no PUBLIC__ override path by design — the AST validator runs on server-config.ts, so a process.env.X read throws at discovery time. For true secrets that must vary per environment (SLAS secrets, Marketing Cloud credentials), keep using process.env from a server route — never put them in server-config.ts.

For more information, see the Extensions documentation in the template GitHub repo.

Testing

The template provides shared test utilities for components and hooks that depend on config:

import {
  mockConfig,
  mockBuildConfig,
  ConfigWrapper,
  createConfigWrapper,
} from "@/test-utils/config";

// Use the default wrapper
renderHook(() => useConfig(), { wrapper: ConfigWrapper });

// Use a wrapper with custom overrides (deep merged)
const CustomWrapper = createConfigWrapper({
  app: {
    pages: {
      cart: {
        maxQuantityPerItem: 5,
      },
    },
  },
});
renderHook(() => useConfig(), { wrapper: CustomWrapper });
  • mockBuildConfig — a full Config object with realistic test values
  • mockConfig — the app section (i.e., mockBuildConfig.app)
  • ConfigWrapper — a ready-to-use wrapper component for renderHook / render
  • createConfigWrapper(overrides?) — creates a wrapper with custom config (deep-merges nested overrides)

For tests that need all providers (config + currency + store locator), use AllProvidersWrapper from @/test-utils/context-provider.

Marketing Cloud Configuration (Server-Only)

Marketing Cloud is used for sending emails in features like passwordless login and password reset. The configuration is optional and only required if you're using these features.

Environment Variables

# Marketing Cloud API Configuration (Server-only - NO PUBLIC__ prefix)
MARKETING_CLOUD_CLIENT_ID=your-client-id
MARKETING_CLOUD_CLIENT_SECRET=your-client-secret
MARKETING_CLOUD_SUBDOMAIN=your-subdomain
MARKETING_CLOUD_PASSWORDLESS_LOGIN_TEMPLATE=your-passwordless-template-id
MARKETING_CLOUD_RESET_PASSWORD_TEMPLATE=your-reset-password-template-id

Important Security Notes:

  • ❌ These variables do NOT have the PUBLIC__ prefix - they are server-only
  • ❌ They are NOT included in config.server.ts or exposed to the client
  • ✅ Read them directly from process.env in server-side code

Deployment

When deploying to Managed Runtime (MRT), set your environment variables in the Runtime Admin.

  1. Log in to the Runtime Admin.
  2. Navigate to your project → Environment Variables.
  3. Add the required PUBLIC__ variables.
  4. Add any server-only secrets without the PUBLIC__ prefix.
  5. Deploy your app.

All the same rules apply: use the PUBLIC__ prefix for client-safe values, use the __ path syntax for nested config, and read server-only secrets directly from process.env.

MRT limits: Variable names max 512 characters, total PUBLIC__ values max 32 KB. Use JSON to consolidate related settings if needed.

Learn more about MRT environment variables →

Common Issues

Changed .env but Nothing Happened?

Restart your dev server. Environment variables are loaded at startup.

Environment Variable Not Working?

  • Verify the variable name starts with PUBLIC__ (double underscore after PUBLIC)
  • Check the .env file is in the project root
  • Ensure that the path exists in config.server.ts—you can only override existing paths
  • For booleans, use string "true" not bare true

Type Errors After Adding Configuration?

Update both src/types/config.ts (types) and config.server.ts (defaults) to match.

App Won't Start—Missing Credentials?

Copy .env.default to .env and set the required B2C Commerce credentials. See Required for the app to start.

Path Validation Error?

The configuration system validates that environment variable paths exist in your config. If you see an error like "local" doesn't exist, check for typos. The system suggests similar valid paths when possible.

Locale or Translation Not Working?

If a locale or translation is missing at runtime, verify:

  1. The locale is included in both supportedLocales for the relevant site in commerce.sites and in i18n.supportedLngs.
  2. Translation files exist for the locale (for example, public/locales/de-DE/translation.json).
  3. The locale ID uses BCP 47 hyphen format (en-US, not en_US).
  4. Review src/middlewares/i18next.ts to confirm the locale is listed in the server-side i18next configuration.

Optional Configuration Variables

The following optional PUBLIC__* environment variables can override the defaults in config.server.ts:

Commerce API Settings

Variable Default Effect
PUBLIC__app__commerce__api__proxy /mobify/proxy/api SCAPI proxy path
PUBLIC__app__commerce__api__callback /callback OAuth callback path
PUBLIC__app__commerce__api__privateKeyEnabled false Use private SLAS client
PUBLIC__app__commerce__api__guestRefreshTokenExpirySeconds from API response Override guest refresh-token TTL
PUBLIC__app__commerce__api__registeredRefreshTokenExpirySeconds from API response Override registered refresh-token TTL

Feature Flags

Variable Default Effect
PUBLIC__app__hybrid__enabled false Hybrid PWA mode
PUBLIC__app__auth__otpLength 6 OTP length (6 or 8)
PUBLIC__app__features__passwordlessLogin__mode email email | callback
PUBLIC__app__features__otpRequest__mode email email | callback
PUBLIC__app__features__resetPassword__mode email email | callback
PUBLIC__app__features__mrtBasedPageDesignerResolution false Resolve PD pages via MRT Data Store
PUBLIC__app__features__socialLogin__enabled true Apple/Google login button
PUBLIC__app__features__socialLogin__callbackUri /social-callback Social login callback path
PUBLIC__app__features__socialLogin__providers ["Apple","Google"] Provider list
PUBLIC__app__features__shopperContext__enabled false Shopper context API
PUBLIC__app__features__googleCloudAPI__apiKey — Google Address Autocomplete

Site Configuration

Variable Default Effect
PUBLIC__app__defaultSiteId (single-site default) Override default site
PUBLIC__app__commerce__sites (single-site default) Multi-site JSON config
PUBLIC__app__cookies__domain host-only Global default cookie domain for all cookies (e.g. .example.com); per-site commerce.sites[].cookies.domain overrides it

Security

Variable Default Effect
PUBLIC__app__security__turnstile__enabled false Turnstile bot protection
PUBLIC__app__security__turnstile__sites — Turnstile per-site configuration

Engagement Adapters

note: Engagement adapter settings (Einstein, Data 360, Active Data) cannot be overridden with PUBLIC__ environment variables. To change adapter settings, update config.server.ts directly. This restriction exists because engagement configuration affects build-time validation for analytics instrumentation.

Enabling Optional Features

Each block in this section is a copy-pasteable env snippet. Drop it into your .env and uncomment to enable the feature.

Multi-site / commerce.sites

Single-site is the default. To enable multiple sites, define them as a JSON array:

# Single-site override
# PUBLIC__app__defaultSiteId=RefArchGlobal

# Multi-site (single-line — works for MRT and local .env)
# PUBLIC__app__commerce__sites=[{"id":"RefArch","defaultLocale":"en-US","defaultCurrency":"USD","supportedLocales":[{"id":"en-US","preferredCurrency":"USD"}],"supportedCurrencies":["USD"]}]

# Multi-site (multi-line — local .env only, easier to read)
# PUBLIC__app__commerce__sites='[
#  {
#    "id": "RefArchGlobal",
#    "defaultLocale": "en-GB",
#    "defaultCurrency": "GBP",
#    "cookies": {"domain": null},
#    "supportedLocales": [
#      {"id": "en-US", "preferredCurrency": "USD"},
#      {"id": "de-DE", "preferredCurrency": "EUR"},
#      {"id": "en-GB", "preferredCurrency": "GBP"}
#    ],
#    "supportedCurrencies": ["EUR", "GBP"]
#  }
# ]'

See Configure Multisite URLs for site-context routing details.

note: For MRT environment variables, convert to a single line and remove the surrounding quotes. Multi-line format works only in local .env files.

Live Sites from the Data Access Layer (commerce.sitesFromDal)

By default, Storefront Next syncs site, locale, and currency data from Business Manager per request via the Data Access Layer (DAL). This means you don't need to edit config.server.ts or redeploy to pick up new sites or locale changes — Business Manager is the source of truth.

To disable DAL-sourced sites and fall back to the static commerce.sites array in config.server.ts:

# PUBLIC__app__commerce__sitesFromDal=false

When sitesFromDal is enabled (the default) and the DAL is unavailable, the storefront automatically falls back to the static commerce.sites configuration. See Configure Multisite URLs for DAL-sourced site fallback rules.

Hybrid Proxy (local development only)

Silent HTTP proxying with cookie rewriting for a unified storefront experience. Local-dev only — production routing should use Cloudflare eCDN. Requires SFCC_ORIGIN and PUBLIC__app__defaultSiteId.

# HYBRID_PROXY_ENABLED=true
# HYBRID_PROXY_LOCALE=en-GB
# HYBRID_ROUTING_RULES='(http.request.uri.path matches "^/$" or http.request.uri.path matches "^/product.*")'
# SFCC_ORIGIN=https://zzrf-001.dx.commercecloud.salesforce.com

See Set Up Hybrid Proxy Locally.

Passwordless Login (Marketing Cloud)

# PUBLIC__app__features__passwordlessLogin__mode=email
# PUBLIC__app__features__passwordlessLogin__callbackUri='/passwordless-login-callback'
# PUBLIC__app__features__passwordlessLogin__landingUri='/login'
# PUBLIC__app__features__otpRequest__mode=email
# PUBLIC__app__features__otpRequest__callbackUri='https://example.com/otp-callback'
# PUBLIC__app__features__resetPassword__mode=email
# PUBLIC__app__features__resetPassword__callbackUri='/reset-password-callback'
# PUBLIC__app__features__resetPassword__landingUri='/reset-password'

When using mode=email, also set the server-only Marketing Cloud secrets (see Marketing Cloud Configuration). See Passwordless Login for Storefront Next.

Turnstile Bot Protection

Cloudflare Turnstile is disabled by default. The test site key below always passes — production sites must set their own keys via MRT env vars.

# PUBLIC__security__turnstile__enabled=true
# PUBLIC__security__turnstile__sites={"local-dev":[{"siteKey":"1x00000000000000000000BB","domains":["localhost","127.0.0.1"]}]}

Commerce Client (Cimulate)

# PUBLIC__app__cimulateAgent='{"enabled":true,"provider":"commerce-client","commerceClientScriptSourceUrl":"https://...","scrt2Url":"https://...","salesforceOrgId":"...","esDeveloperName":"..."}'

Set as a single JSON string. Required fields: enabled, commerceClientScriptSourceUrl, scrt2Url, salesforceOrgId, esDeveloperName. See src/components/cimulate/README.md for setup.

See Shopping Agent for Storefront Next for environment-specific setup.

Cookie Domain

Sets the default Domain on every cookie the storefront writes — auth/session and site-context (site_id, locale, currency). A per-site commerce.sites[].cookies.domain overrides it for that site. Unset = host-only scoping; setting a domain is opt-in.

# PUBLIC__app__cookies__domain=.example.com

See the Cookie Domain Configuration guide for the full guide, including the matching Business Manager setting and rollout guidance.

Refresh-Token Expiry Overrides

If unset, the storefront uses the expiry returned by SCAPI.

# PUBLIC__app__commerce__api__guestRefreshTokenExpirySeconds=2592000        # ~30 days
# PUBLIC__app__commerce__api__registeredRefreshTokenExpirySeconds=7776000   # ~90 days

Google Cloud API Key (Address Autocomplete)

# PUBLIC__app__features__googleCloudAPI__apiKey=

Logging

# SFCC_LOG_LEVEL=info   # error | warn | info | debug

Shared with the SDK logger (storefront-next-dev) for unified control.

Managed Runtime deployment vars

Already in .env.default — listed here for completeness.

MRT_PROJECT=my-project-slug
MRT_TARGET=development

Server-Only SLAS Secret

# COMMERCE_API_SLAS_SECRET=your-secret-here

Read directly from process.env in server-side code (loaders, actions, middleware). Never prefix with PUBLIC__.

JSON configuration pattern

Complex values can be encoded as JSON strings—the merge mechanism parses any value that looks like JSON.

# Override multiple cart configuration values at once
# PUBLIC__app__pages__cart='{"quantityUpdateDebounce":1000,"maxQuantityPerItem":500,"enableSaveForLater":true}'

Configuration Options Reference

This section provides detailed documentation for all configuration options available in config.server.ts. For the complete reference with all options, descriptions, defaults, and examples, see the Configuration Options Reference in the template GitHub repo.

Key configuration categories include:

  • metadata - Project identification and metadata
  • runtime - Runtime deployment settings for MRT
  • app - Application-specific configuration
    • pages - Page-specific settings (navigation, cart, search, home)
    • commerce - B2C Commerce API details and site configuration
    • hybrid - Hybrid mode configuration
    • auth - Authentication configuration (OTP length, etc.)
    • security - Security headers and Turnstile configuration
    • features - Feature flags (passwordless login, social login, etc.)
    • i18n - Internationalization settings
    • global - Global UI and component settings (branding, badges, recommendations)
    • links - Link hints for browser resource loading
    • images - Dynamic Imaging Service settings
    • search - Search-specific settings
    • performance - Performance optimization settings
    • engagement - Analytics and engagement adapters
    • commerceAgent - Shopper Agent (Embedded Messaging / Agentforce)
    • development - Development tools and features

Notable Configuration Options

The following options are commonly needed but not covered elsewhere in this guide.

Passkey (WebAuthn) Feature Flags

Option Type Default Notes
features.passkey.enabled boolean false Requires sfcc.pwdless_login scope on your SLAS client.
features.passkey.mode 'email' | 'callback' email SLAS does not support 'sms' mode for passkey authorization.
features.passkey.callbackUri string — Callback URI for passkey authorization redirect flow.

Guest Checkout

Option Type Default Notes
features.guestCheckout boolean true Allow unauthenticated checkout

Social Share

Option Type Default Notes
features.socialShare.enabled boolean true Enable product social sharing
features.socialShare.providers string[] ['Twitter', 'Facebook', 'LinkedIn', 'Email'] List of share provider names

Shopper Context Source Code Suffix

Option Type Default Notes
features.shopperContext.dwsourcecodeCookieSuffix string — Cookie suffix for campaign attribution via shopper context source code

Category Page Pagination (src/lib/config.ui.ts)

The PLP (Product Listing Page) pagination is configured in src/lib/config.ui.ts rather than config.server.ts and cannot be overridden via PUBLIC__ env vars.

Option Type Default Notes
uiConfig.pages.category.pagination.mode 'load-more' | 'traditional' 'load-more' Pagination UI mode
uiConfig.pages.category.pagination.batchSize number 25 Products per page/load-more batch
uiConfig.pages.category.pagination.mobileBatchSize number 25 Products per batch on mobile
uiConfig.pages.category.pagination.maxProducts number 200 Maximum total products rendered

Engagement Adapter Options

Engagement adapter settings must be configured directly in config.server.ts. They cannot be overridden via PUBLIC__ environment variables because engagement configuration affects build-time validation for analytics instrumentation.

Option Notes
engagement.adapters[].consentCategory Consent category required before firing events
engagement.adapters[].eventToggles Per-event on/off switches
engagement.adapters[].webStoreId Web store ID required for Data 360 adapter

See Also