---
schema_version: "1"
id: e6b2f8d84cdb49a90d723d4bd58bc0615f24ec2e969628db874c225a9464304e
title: Project Configuration
collection: developer
version: current
locale: en-us
portal_url: https://docs.salesforce.com/developer/current/en-us/commerce-cloud/sfnext/guides/sfnext-project-config.md
canonical_url: https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/sfnext-project-config.html
source_site: developer.salesforce.com
content_type: documentation
content_hash: 3a669827cddde0401ab08812340943e767d08731729aead59a22c380042535ca
products:
  - Composable Storefront
  - Salesforce APIs
---
# 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()`

```typescript
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()`

```typescript
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.

```typescript
// 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](#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`:

```typescript
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`)             |

```bash
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](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/sfnext-cli.md).

### 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](https://developer.salesforce.com/docs/commerce/commerce-api/guide/authorization-for-shopper-apis.html).

### 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](#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:

- [Configure Multisite URLs](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/sfnext-multi-site.md) — sites, locales, currencies, and URL patterns
- [Set Up Hybrid Proxy Locally](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/sfnext-hybrid-proxy-local-dev.md) — `HYBRID_PROXY_*`, `SFCC_ORIGIN`, routing rules
- [Passwordless Login for Storefront Next](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/sfnext-passwordless-login.md) — passwordless login, OTP request, and reset password modes
- [Shopper Context](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/sfnext-shopper-context.md) — qualifier-based personalization
- [Shopping Agent for Storefront Next](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/sfnext-shopping-agent.md) — embedded chat configuration

## 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:

```bash
# 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:

```bash
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:

```bash
# 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`:

```bash
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:

```bash
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:

```bash
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:

```bash
# ❌ 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

```bash
# ✅ 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`:

```typescript
// 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:

```typescript
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`)

```typescript
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

```bash
# 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:**

```typescript
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:**

```typescript
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

```json
{
  "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`.

```bash
# .env - Server-only secret (no PUBLIC__ prefix)
COMMERCE_API_SLAS_SECRET=your-secret-here
```

```typescript
// 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.

```bash
# 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:

```bash
# 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:

```bash
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](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/sfnext-multi-site.md).

## Accessing Configuration

### In Loaders and Actions

Use `getConfig()` with the router context.

```typescript
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:

```typescript
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.

```typescript
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`:

```typescript
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`:

```typescript
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`:

```typescript
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:

```bash
PUBLIC__app__myFeature__enabled=true
PUBLIC__app__myFeature__maxItems=20
```

### 4. Use in Your Code

**In React components:**

```typescript
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:**

```typescript
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:

```json
{
  "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.

```typescript
// src/extensions/my-feature/config.ts
export default {
  enabled: false,
  timeout: 5000,
};
```

Merchants override per environment with `PUBLIC__app__extension__<key>__<setting>`:

```bash
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`:

```typescript
// src/extensions/loqate-address-verification/server-config.ts
export default {
  scapiOverride: "",
  retryBudget: 3,
};
```

Read the value from a server loader, action, or middleware:

```typescript
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](https://github.com/SalesforceCommerceCloud/storefront-next-template/blob/latest/src/extensions/README.md) in the template GitHub repo.

## Testing

The template provides shared test utilities for components and hooks that depend on config:

```typescript
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

```bash
# 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 →](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/environment-variables.html)

## 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](#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:

```bash
# 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](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/sfnext-multi-site.md) 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`:

```bash
# 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](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/sfnext-multi-site.md) 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`.

```bash
# 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](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/sfnext-hybrid-proxy-local-dev.md).

### Passwordless Login (Marketing Cloud)

```bash
# 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](#marketing-cloud-configuration-server-only)). See [Passwordless Login for Storefront Next](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/sfnext-passwordless-login.md).

### 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.

```bash
# PUBLIC__security__turnstile__enabled=true
# PUBLIC__security__turnstile__sites={"local-dev":[{"siteKey":"1x00000000000000000000BB","domains":["localhost","127.0.0.1"]}]}
```

### Commerce Client (Cimulate)

```bash
# 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](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/sfnext-shopping-agent.md) 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.

```bash
# PUBLIC__app__cookies__domain=.example.com
```

See the [Cookie Domain Configuration guide](https://github.com/SalesforceCommerceCloud/storefront-next-template/blob/latest/docs/README-COOKIE-DOMAIN.md) 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.

```bash
# PUBLIC__app__commerce__api__guestRefreshTokenExpirySeconds=2592000        # ~30 days
# PUBLIC__app__commerce__api__registeredRefreshTokenExpirySeconds=7776000   # ~90 days
```

### Google Cloud API Key (Address Autocomplete)

```bash
# PUBLIC__app__features__googleCloudAPI__apiKey=
```

### Logging

```bash
# 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.

```bash
MRT_PROJECT=my-project-slug
MRT_TARGET=development
```

### Server-Only SLAS Secret

```bash
# 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.

```bash
# 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](https://github.com/SalesforceCommerceCloud/storefront-next-template/blob/latest/docs/README-CONFIG-OPTIONS.md) 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**

- [Security Response Headers](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/sfnext-security-headers.md)
