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() 

1import { getConfig } from "@salesforce/storefront-next-runtime/config";
2
3// Server loader/action - pass context
4export function loader({ context }: LoaderFunctionArgs) {
5  const config = getConfig(context);
6  return { limit: config.search.products.hits.limit };
7}
8
9// Client loader - no context needed
10export function clientLoader() {
11  const config = getConfig();
12  return { limit: config.search.products.hits.limit };
13}

For React Components: useConfig() 

1import { useConfig } from "@salesforce/storefront-next-runtime/config";
2
3function ProductGrid() {
4  const config = useConfig();
5  return <div>Showing {config.search.products.hits.limit} products</div>;
6}

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.

Note

Configuration File 

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

1// config.server.ts
2import { defineConfig } from "@salesforce/storefront-next-runtime/config";
3import type { Config } from "./src/types/config";
4
5export default defineConfig<Config>({
6  metadata: {
7    projectName: "My Storefront",
8    projectSlug: "my-storefront",
9  },
10  runtime: {
11    ssrOnly: [],
12    ssrParameters: { ssrFunctionNodeVersion: "24.x" },
13  },
14  app: {
15    commerce: {
16      /* Commerce API settings */
17    },
18    defaultSiteId: "RefArch",
19    /* Locale, currency, features */
20    pages: {
21      /* Page-specific settings */
22    },
23    global: {
24      /* Branding, product listing defaults */
25    },
26    images: {
27      /* Image optimization */
28    },
29    performance: {
30      /* Caching */
31    },
32    engagement: {
33      /* Analytics adapters */
34    },
35  },
36});

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

The configuration is organized into three main sections.

SectionPurposeClient Access
metadataProject name and slug for deploymentServer only
runtimeSSR and deployment settingsServer only
appApplication 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.

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.

Warning

How Configuration Works 

The configuration system follows this flow:

  1. Types defined in src/types/config.tsAppConfig 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:

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

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:

VariablePurpose
PUBLIC__app__commerce__api__clientIdSLAS client ID provisioned in B2C Commerce
PUBLIC__app__commerce__api__organizationIdB2C Commerce organization or realm ID (for example, f_ecom_zzrf_001)
PUBLIC__app__commerce__api__shortCodeSCAPI short code for your tenant (for example, kv7kzm78)
1PUBLIC__app__commerce__api__clientId=your-client-id
2PUBLIC__app__commerce__api__organizationId=your-org-id
3PUBLIC__app__commerce__api__shortCode=your-short-code

Required for pnpm push (Managed Runtime deploy) 

VariableDefaultNotes
MRT_PROJECTfalls back to package.json#nameMRT project slug. Owned by the MRT/Fast Setup team.
MRT_TARGETMRT deploy target (for example, development, production).

For more information, see Storefront Next CLI.

Server-only secrets (never prefix with PUBLIC__) 

VariableUsed byNotes
COMMERCE_API_SLAS_SECRETsrc/lib/api-clients.server.ts, e2e/src/utils/scapi-helper.tsRequired only with private-client SCAPI auth.
GUEST_ORDER_LOOKUP_COOKIE_SECRETsrc/lib/order/session.server.tsSigns 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_URLPasswordless login email deliveryRequired only when passwordlessLogin.mode = 'email' and you ship your own MC tenant.
SCAPI_PROXY_HOSTvite-plugins/env-validation.ts, src/middlewares/app-config.server.tsInternal-developer-only override (workspace proxy).

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.

Warning

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 

VariableDefaultEffect
HYBRID_PROXY_ENABLEDfalseEnable Vite hybrid proxy
HYBRID_ROUTING_RULESCloudflare-style routing expression for hybrid proxy
HYBRID_PROXY_LOCALEfalls back to i18n.fallbackLngLocale for SFRA path transformation
SFCC_ORIGINSFCC origin URL (required when hybrid proxy enabled)
SFCC_LOG_LEVELwarn (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:

1# This environment variable:
2PUBLIC__app__commerce__api__clientId=abc123
3
4# Maps to this config path:
5config.app.commerce.api.clientId
6
7# Which creates this structure:
8{
9  app: {
10    commerce: {
11      api: {
12        clientId: 'abc123'
13      }
14    }
15  }
16}

Value Types 

Values are automatically parsed to the correct type:

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

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

1# Instead of setting each value separately:
2PUBLIC__app__myFeature__option1=value1
3PUBLIC__app__myFeature__option2=value2
4PUBLIC__app__myFeature__nested__enabled=true
5
6# Use a single JSON value:
7PUBLIC__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:

1PUBLIC__app__commerce__api__clientId=abc123    # ✅ Works
2PUBLIC__APP__COMMERCE__API__CLIENTID=abc123    # ✅ Also works
3PUBLIC__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:

1PUBLIC__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:

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

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

1# ❌ Too deep (11 levels):
2PUBLIC__a__b__c__d__e__f__g__h__i__j__k=value
3
4# ✅ Use JSON instead:
5PUBLIC__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
1# ✅ Safe to expose to client:
2PUBLIC__app__commerce__api__clientId=abc123
3
4# ✅ Server-only secret (no PUBLIC__ prefix):
5COMMERCE_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:

1// config.server.ts (defaults)
2export default defineConfig({
3  app: {
4    myFeature: {
5      debounce: 750,
6      maxItems: 999,
7      enabled: true,
8    }
9  }
10});
11
12// With env var:
13// PUBLIC__app__myFeature__debounce=1000
14
15// Final result:
16{
17  app: {
18    myFeature: {
19      debounce: 1000,        // ← overridden
20      maxItems: 999,         // ← preserved
21      enabled: true,         // ← preserved
22    }
23  }
24}

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:

1import type { BaseConfig } from "@salesforce/storefront-next-runtime/config";
2import type { Site, Url } from "@salesforce/storefront-next-runtime/config";
3
4// Define all app fields in one flat type
5export type AppConfig = {
6  commerce: { api: { clientId: string /* ... */ }; sites: Array<Site> };
7  defaultSiteId: string;
8  url?: Url;
9  myFeature: {
10    enabled: boolean;
11    maxItems: number;
12  };
13  // ...other template-specific fields (pages, features, global, etc.)
14};
15
16// Full config type used by config.server.ts
17export type Config = BaseConfig<AppConfig>;

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

1import { defineConfig } from "@salesforce/storefront-next-runtime/config";
2import type { Config } from "./src/types/config";
3
4export default defineConfig<Config>({
5  metadata: { projectName: "My Store", projectSlug: "my-store" },
6  app: {
7    // SCAPI fields
8    commerce: { api: { clientId: "", organizationId: "", siteId: "", shortCode: "" }, sites: [] },
9    defaultSiteId: "RefArch",
10    // Template-specific fields
11    myFeature: {
12      enabled: false, // Just the default - no process.env needed!
13      maxItems: 10,
14    },
15  },
16});

3. Override via environment variables 

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

4. Use it in your code 

In React components:

1import { useConfig } from "@salesforce/storefront-next-runtime/config";
2
3export function MyComponent() {
4  const config = useConfig();
5
6  if (config.myFeature.enabled) {
7    const maxItems = config.myFeature.maxItems;
8    // Your feature code here
9  }
10}

In loaders/actions:

1import { getConfig } from "@salesforce/storefront-next-runtime/config";
2
3export function loader({ context }: LoaderFunctionArgs) {
4  const config = getConfig(context);
5
6  if (config.myFeature.enabled) {
7    // Your loader code here
8  }
9}

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
1{
2  "configs": [
3    {
4      "name": "SLAS Client ID",
5      "key": "PUBLIC__app__commerce__api__clientId"
6    },
7    {
8      "name": "Organization ID",
9      "key": "PUBLIC__app__commerce__api__organizationId"
10    },
11    {
12      "name": "Short Code",
13      "key": "PUBLIC__app__commerce__api__shortCode"
14    }
15  ]
16}

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.

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

Important

Use PUBLIC__ ForUse Non-Prefixed For
Client IDsAPI secrets
Site IDsPrivate keys
Locales and currenciesDatabase credentials
Feature flagsAuthentication tokens
Public API endpointsSLAS secrets

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

1# .env - Server-only secret (no PUBLIC__ prefix)
2COMMERCE_API_SLAS_SECRET=your-secret-here
1// In server-side code only
2const 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.

1# Example: one site with multiple locales and currencies (single line)
2PUBLIC__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:

1# Multi-line format (easier to read and edit)
2PUBLIC__app__commerce__sites='[
3  {
4    "id": "RefArchGlobal",
5    "defaultLocale": "en-GB",
6    "defaultCurrency": "GBP",
7    "cookies": {"domain": null},
8    "supportedLocales": [
9      {"id": "en-US", "preferredCurrency": "USD"},
10      {"id": "de-DE", "preferredCurrency": "EUR"},
11      {"id": "en-GB", "preferredCurrency": "GBP"}
12    ],
13    "supportedCurrencies": ["EUR", "GBP", "USD"]
14  }
15]'

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:

1PUBLIC__app__commerce__sites='[
2  {
3    "id": "SiteA",
4    "cookies": {"domain": ".example.com"},
5    ...
6  },
7  {
8    "id": "SiteB",
9    "cookies": {"domain": ".other-domain.com"},
10    ...
11  }
12]'

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

Accessing Configuration 

In Loaders and Actions 

Use getConfig() with the router context.

1import { getConfig } from "@salesforce/storefront-next-runtime/config";
2
3export function loader({ context }: LoaderFunctionArgs) {
4  const config = getConfig(context);
5  const { clientId, siteId } = config.commerce.api;
6  return { siteId };
7}
8
9export async function action({ context, request }: ActionFunctionArgs) {
10  const config = getConfig(context);
11  // Use config values...
12}

In React Components 

Use the useConfig() hook:

1import { useConfig } from "@salesforce/storefront-next-runtime/config";
2
3function Header() {
4  const config = useConfig();
5  return <h1>{config.global.branding.name}</h1>;
6}

In Client Loaders 

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

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

In Custom Middleware 

Custom middleware can read the resolved app config from appConfigContext:

1import { appConfigContext } from "@salesforce/storefront-next-runtime/config";
2
3const 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:

1import type { BaseConfig } from "@salesforce/storefront-next-runtime/config";
2import type { Site, Url } from "@salesforce/storefront-next-runtime/config";
3
4// Define all app fields in one flat type
5export type AppConfig = {
6  commerce: { api: { clientId: string /* ... */ }; sites: Array<Site> };
7  defaultSiteId: string;
8  url?: Url;
9  myFeature: {
10    enabled: boolean;
11    maxItems: number;
12  };
13  // ...other template-specific fields (pages, features, global, etc.)
14};
15
16// Full config type used by config.server.ts
17export type Config = BaseConfig<AppConfig>;

2. Add Default Value 

Add the default in config.server.ts:

1import { defineConfig } from "@salesforce/storefront-next-runtime/config";
2import type { Config } from "./src/types/config";
3
4export default defineConfig<Config>({
5  metadata: { projectName: "My Store", projectSlug: "my-store" },
6  app: {
7    // SCAPI fields
8    commerce: { api: { clientId: "", organizationId: "", siteId: "", shortCode: "" }, sites: [] },
9    defaultSiteId: "RefArch",
10    // Template-specific fields
11    myFeature: {
12      enabled: false, // Just the default - no process.env needed!
13      maxItems: 10,
14    },
15  },
16});

3. Override via Environment Variables 

No code changes needed. Use the PUBLIC__ prefix:

1PUBLIC__app__myFeature__enabled=true
2PUBLIC__app__myFeature__maxItems=20

4. Use in Your Code 

In React components:

1import { useConfig } from "@salesforce/storefront-next-runtime/config";
2
3export function MyComponent() {
4  const config = useConfig();
5
6  if (config.myFeature.enabled) {
7    const maxItems = config.myFeature.maxItems;
8    // Your feature code here
9  }
10}

In loaders/actions:

1import { getConfig } from "@salesforce/storefront-next-runtime/config";
2
3export function loader({ context }: LoaderFunctionArgs) {
4  const config = getConfig(context);
5
6  if (config.myFeature.enabled) {
7    // Your loader code here
8  }
9}

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

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

1{
2  "configs": [
3    {
4      "name": "SLAS Client ID",
5      "key": "PUBLIC__app__commerce__api__clientId"
6    },
7    {
8      "name": "My Feature Max Items",
9      "key": "PUBLIC__app__myFeature__maxItems"
10    }
11  ]
12}

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.

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

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

1PUBLIC__app__extension__myFeature__enabled=true
2PUBLIC__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:

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

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

1import { getConfig } from "@salesforce/storefront-next-runtime/config";
2
3export function loader({ context }: LoaderFunctionArgs) {
4  const { scapiOverride } = getConfig(context).serverExtension?.loqateAddressVerification ?? {};
5  // …
6}

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:

1import {
2  mockConfig,
3  mockBuildConfig,
4  ConfigWrapper,
5  createConfigWrapper,
6} from "@/test-utils/config";
7
8// Use the default wrapper
9renderHook(() => useConfig(), { wrapper: ConfigWrapper });
10
11// Use a wrapper with custom overrides (deep merged)
12const CustomWrapper = createConfigWrapper({
13  app: {
14    pages: {
15      cart: {
16        maxQuantityPerItem: 5,
17      },
18    },
19  },
20});
21renderHook(() => 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 

1# Marketing Cloud API Configuration (Server-only - NO PUBLIC__ prefix)
2MARKETING_CLOUD_CLIENT_ID=your-client-id
3MARKETING_CLOUD_CLIENT_SECRET=your-client-secret
4MARKETING_CLOUD_SUBDOMAIN=your-subdomain
5MARKETING_CLOUD_PASSWORDLESS_LOGIN_TEMPLATE=your-passwordless-template-id
6MARKETING_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 

VariableDefaultEffect
PUBLIC__app__commerce__api__proxy/mobify/proxy/apiSCAPI proxy path
PUBLIC__app__commerce__api__callback/callbackOAuth callback path
PUBLIC__app__commerce__api__privateKeyEnabledfalseUse private SLAS client
PUBLIC__app__commerce__api__guestRefreshTokenExpirySecondsfrom API responseOverride guest refresh-token TTL
PUBLIC__app__commerce__api__registeredRefreshTokenExpirySecondsfrom API responseOverride registered refresh-token TTL

Feature Flags 

VariableDefaultEffect
PUBLIC__app__hybrid__enabledfalseHybrid PWA mode
PUBLIC__app__auth__otpLength6OTP length (6 or 8)
PUBLIC__app__features__passwordlessLogin__modeemailemail | callback
PUBLIC__app__features__otpRequest__modeemailemail | callback
PUBLIC__app__features__resetPassword__modeemailemail | callback
PUBLIC__app__features__mrtBasedPageDesignerResolutionfalseResolve PD pages via MRT Data Store
PUBLIC__app__features__socialLogin__enabledtrueApple/Google login button
PUBLIC__app__features__socialLogin__callbackUri/social-callbackSocial login callback path
PUBLIC__app__features__socialLogin__providers["Apple","Google"]Provider list
PUBLIC__app__features__shopperContext__enabledfalseShopper context API
PUBLIC__app__features__googleCloudAPI__apiKeyGoogle Address Autocomplete

Site Configuration 

VariableDefaultEffect
PUBLIC__app__defaultSiteId(single-site default)Override default site
PUBLIC__app__commerce__sites(single-site default)Multi-site JSON config
PUBLIC__app__cookies__domainhost-onlyGlobal default cookie domain for all cookies (e.g. .example.com); per-site commerce.sites[].cookies.domain overrides it

Security 

VariableDefaultEffect
PUBLIC__app__security__turnstile__enabledfalseTurnstile bot protection
PUBLIC__app__security__turnstile__sitesTurnstile per-site configuration

Engagement Adapters 

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.

Note

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:

1# Single-site override
2# PUBLIC__app__defaultSiteId=RefArchGlobal
3
4# Multi-site (single-line — works for MRT and local .env)
5# PUBLIC__app__commerce__sites=[{"id":"RefArch","defaultLocale":"en-US","defaultCurrency":"USD","supportedLocales":[{"id":"en-US","preferredCurrency":"USD"}],"supportedCurrencies":["USD"]}]
6
7# Multi-site (multi-line — local .env only, easier to read)
8# PUBLIC__app__commerce__sites='[
9#  {
10#    "id": "RefArchGlobal",
11#    "defaultLocale": "en-GB",
12#    "defaultCurrency": "GBP",
13#    "cookies": {"domain": null},
14#    "supportedLocales": [
15#      {"id": "en-US", "preferredCurrency": "USD"},
16#      {"id": "de-DE", "preferredCurrency": "EUR"},
17#      {"id": "en-GB", "preferredCurrency": "GBP"}
18#    ],
19#    "supportedCurrencies": ["EUR", "GBP"]
20#  }
21# ]'

See Configure Multisite URLs for site-context routing details.

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

Note

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:

1# 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.

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

See Set Up Hybrid Proxy Locally.

Passwordless Login (Marketing Cloud) 

1# PUBLIC__app__features__passwordlessLogin__mode=email
2# PUBLIC__app__features__passwordlessLogin__callbackUri='/passwordless-login-callback'
3# PUBLIC__app__features__passwordlessLogin__landingUri='/login'
4# PUBLIC__app__features__otpRequest__mode=email
5# PUBLIC__app__features__otpRequest__callbackUri='https://example.com/otp-callback'
6# PUBLIC__app__features__resetPassword__mode=email
7# PUBLIC__app__features__resetPassword__callbackUri='/reset-password-callback'
8# 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.

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

Commerce Client (Cimulate) 

1# 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.

1# 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.

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

Google Cloud API Key (Address Autocomplete) 

1# PUBLIC__app__features__googleCloudAPI__apiKey=

Logging 

1# 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.

1MRT_PROJECT=my-project-slug
2MRT_TARGET=development

Server-Only SLAS Secret 

1# 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.

1# Override multiple cart configuration values at once
2# 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 

OptionTypeDefaultNotes
features.passkey.enabledbooleanfalseRequires sfcc.pwdless_login scope on your SLAS client.
features.passkey.mode'email' | 'callback'emailSLAS does not support 'sms' mode for passkey authorization.
features.passkey.callbackUristringCallback URI for passkey authorization redirect flow.

Guest Checkout 

OptionTypeDefaultNotes
features.guestCheckoutbooleantrueAllow unauthenticated checkout

Social Share 

OptionTypeDefaultNotes
features.socialShare.enabledbooleantrueEnable product social sharing
features.socialShare.providersstring[]['Twitter', 'Facebook', 'LinkedIn', 'Email']List of share provider names

Shopper Context Source Code Suffix 

OptionTypeDefaultNotes
features.shopperContext.dwsourcecodeCookieSuffixstringCookie 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.

OptionTypeDefaultNotes
uiConfig.pages.category.pagination.mode'load-more' | 'traditional''load-more'Pagination UI mode
uiConfig.pages.category.pagination.batchSizenumber25Products per page/load-more batch
uiConfig.pages.category.pagination.mobileBatchSizenumber25Products per batch on mobile
uiConfig.pages.category.pagination.maxProductsnumber200Maximum 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.

OptionNotes
engagement.adapters[].consentCategoryConsent category required before firing events
engagement.adapters[].eventTogglesPer-event on/off switches
engagement.adapters[].webStoreIdWeb store ID required for Data 360 adapter

See Also