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.
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.
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:
Types defined in src/types/config.ts — AppConfig defines all app fields, Config = BaseConfig<AppConfig>
Defaults defined in config.server.ts — clean, with no process.env references
Environment variables with PUBLIC__ prefix are automatically merged by defineConfig() — this happens at server startup
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:
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)
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.
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.
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=abc12334# Maps to this config path:5config.app.commerce.api.clientId67# 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:
You can also set entire nested objects at once using JSON:
1# Instead of setting each value separately:2PUBLIC__app__myFeature__option1=value13PUBLIC__app__myFeature__option2=value24PUBLIC__app__myFeature__nested__enabled=true56# 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 # ✅ Works2PUBLIC__APP__COMMERCE__API__CLIENTID=abc123 # ✅ Also works3PUBLIC__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=value34# ✅ 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=abc12334# ✅ 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:
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";34// Define all app fields in one flat type5export 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};1516// Full config type used by config.server.ts17export type Config = BaseConfig<AppConfig>;
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__ 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.
1# .env - Server-only secret (no PUBLIC__ prefix)2COMMERCE_API_SLAS_SECRET=your-secret-here
1// In server-side code only2const 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"]}]'
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:
1import{useConfig}from "@salesforce/storefront-next-runtime/config";23export function MyComponent(){4 const config = useConfig();56 if(config.myFeature.enabled){7 const maxItems = config.myFeature.maxItems;8 // Your feature code here9}10}
In loaders/actions:
1import{getConfig}from "@salesforce/storefront-next-runtime/config";23export function loader({context}: LoaderFunctionArgs){4 const config = getConfig(context);56 if(config.myFeature.enabled){7 // Your loader code here8}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.
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:
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:
The client config extractor (src/lib/app-config-client.ts) strips app.serverExtension before writing window.__APP_CONFIG__.
A Vite plugin (vite-plugins/server-only-config-guard.ts) fails the build if any client chunk imports src/extensions/config/server.
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.
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";78// Use the default wrapper9renderHook(()=> useConfig(), {wrapper: ConfigWrapper});1011// 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-id3MARKETING_CLOUD_CLIENT_SECRET=your-client-secret4MARKETING_CLOUD_SUBDOMAIN=your-subdomain5MARKETING_CLOUD_PASSWORDLESS_LOGIN_TEMPLATE=your-passwordless-template-id6MARKETING_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.
Log in to the Runtime Admin.
Navigate to your project → Environment Variables.
Add the required PUBLIC__ variables.
Add any server-only secrets without the PUBLIC__ prefix.
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.
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:
The locale is included in both supportedLocales for the relevant site in commerce.sites and in i18n.supportedLngs.
Translation files exist for the locale (for example, public/locales/de-DE/translation.json).
The locale ID uses BCP 47 hyphen format (en-US, not en_US).
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:
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
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 override2# PUBLIC__app__defaultSiteId=RefArchGlobal34# 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"]}]67# 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# ]'
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.
Set as a single JSON string. Required fields: enabled, commerceClientScriptSourceUrl, scrt2Url, salesforceOrgId, esDeveloperName. See src/components/cimulate/README.md for setup.
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.
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 once2# 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.
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.
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.