---
schema_version: "1"
id: dc58615b482922972df17acdac8a3b6ae3a47d52d483cb4557f3ad07840a8da9
title: Building Your Commerce App
collection: developer
version: current
locale: en-us
portal_url: https://docs.salesforce.com/developer/current/en-us/commerce-cloud/b2c-commerce/guides/commerce-apps/building-your-app.md
canonical_url: https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/building-your-app.html
source_site: developer.salesforce.com
content_type: documentation
content_hash: fb0832a2b477767b674fbde6ca92d7ac37190878694ba4be00a133f4b9a322a1
products:
  - Composable Storefront
  - Salesforce APIs
---
# Building Your Commerce App

Use this guide to build your Commerce App. Choose the architecture that matches your domain — UI-only, Backend-only, or Fullstack — and follow the guidance below.

## Getting Started with Claude Code

The Commerce Apps repository includes Claude Code skills to accelerate development. Start with `/scaffold-app` to generate a complete app directory structure with templates for your chosen architecture (UI-only, Backend-only, or Fullstack). The scaffold skill prompts for your app's domain, name, and ISV details, then generates all required files including `commerce-app.json`, `tasksList.json`, and starter IMPEX files.

For IMPEX generation (`/generate-service-impex`, `/generate-site-preferences-impex`, `/generate-custom-object-impex`) and incremental IMPEX validation (`/validate-impex`), see the full catalog in [Development Environment Setup — Claude Code Skills](/docs/commerce/b2c-commerce/guide/development-environment.md#claude-code-skills).

## Backend-Only Apps

Backend-only apps implement platform-defined extension point hooks. See [The Three Architectures](/docs/commerce/b2c-commerce/guide/architecture.md#the-three-architectures-for-commerce-apps) for an overview.

### Extension Points

If your domain has a platform-defined extension point (currently Tax and OMS), your backend-only or fullstack app will include hook implementations that fulfill the interface contract. Each extension point defines a contract: when the platform calls your code, what data it passes, and what response it expects. Your cartridge provides script implementations for these hooks.

For example, for the Tax domain, you implement:

- `sfcc.app.tax.calculate`: Receives a `dw.order.LineItemCtnr` (basket). Called on **every basket operation** (adding items, changing quantities, updating shipping address, and so on), not only at checkout. Returns `dw.system.Status` or `null` for success. **Both returning `Status.ERROR` and throwing an exception always block the basket operation** — there is no non-blocking error path for this hook.
- `sfcc.app.tax.commit`: Receives a `dw.order.Order` (status `CREATED`). Called immediately after successful order creation. Returns `dw.system.Status` or `null` for success. Returning `Status.ERROR` is non-blocking (logged as a warning, order proceeds); throwing an exception is blocking (rolls back order creation).
- `sfcc.app.tax.cancel`: Receives a `dw.order.Order`. Called during `OrderMgr.failOrder()` or `OrderMgr.cancelOrder()`. Returns `dw.system.Status` or `null` for success. Returning `Status.ERROR` is non-blocking (logged as a warning, cancellation proceeds); throwing an exception is blocking (rolls back the cancellation).

Unlike legacy hooks (where all integrations share `dw.order.calculateTax` and conflict), these domain-specific extension points are resolved through the Extension Registry, not cartridge path scanning. Only the registered provider for a given domain is called.

> **Important:** App-specific extension points (for example, `sfcc.app.tax.*`) can only be invoked through the Commerce Apps installation process. You can't implement these hooks outside of a Commerce App — the platform gates their invocation on the app being installed and its feature toggles being enabled.

> **Reference:** The `sfcc.app.tax.*` extension points will be included in the [26.6 Script API documentation](https://salesforcecommercecloud.github.io/b2c-dev-doc/) with full contract details, parameter types, and return value specifications. Any new extension points introduced for Commerce Apps in future releases will also be documented there. For the OMS domain (`oms`), implement `sfcc.app.oms.order.retrieve`, `.list`, `.cancel`, and `.return`. See [Connect a Third-Party OMS](/docs/commerce/b2c-commerce/guide/connect-third-party-oms.md) for the full contract.

Example `hooks.json` structure:

```json
{
  "hooks": [
    {
      "name": "sfcc.app.tax.calculate",
      "script": "./hooks/taxCalculate"
    },
    {
      "name": "sfcc.app.tax.commit",
      "script": "./hooks/taxCommit"
    },
    {
      "name": "sfcc.app.tax.cancel",
      "script": "./hooks/taxCancel"
    }
  ]
}
```

> **Important — Function naming convention:** The exported JavaScript function name in each hook script **must match the last segment of the extension point name**. For example, the script referenced by `sfcc.app.tax.calculate` must export a function named `calculate`; the script for `sfcc.app.tax.commit` must export `commit`; and so on.
>
> ```js
> // hooks/taxCalculate.js
> exports.calculate = function(basket) { ... };
> ```

> **Important — SiteGenesis and SFRA cartridges are incompatible with Commerce Apps:** SiteGenesis (`sitegenesis_storefront_controllers`, `sitegenesis_storefront_core`) and SFRA (`app_storefront_base`) cartridges override the `dw.order.calculate` hook, which **bypasses the platform's Commerce App tax hook selection entirely**. These cartridges must be **removed** from your site's cartridge path before installing or running a Commerce App. Commerce Apps (CAP) are designed for use with Storefront Next and aren't compatible with SiteGenesis or SFRA storefronts.

#### Example: non-blocking error (returning `Status.ERROR`)

For most hooks, returning `Status.ERROR` is non-blocking — the platform logs a warning and allows the operation to continue. Use this approach when the failure should not prevent the shopper from completing their action. Note that some hooks (such as `sfcc.app.tax.calculate`) block regardless of whether you return `Status.ERROR` or throw an exception — check the extension point contract for your domain.

```js
"use strict";

var Status = require("dw/system/Status");
var Logger = require("dw/system/Logger");

var log = Logger.getLogger("myapp", "hooks");

exports.commit = function (order) {
  var result = callExternalProvider(order);

  if (!result.success) {
    // Non-blocking: the platform logs this as a warning and the operation proceeds.
    log.warn("Provider returned error for order {0}: {1}", order.orderNo, result.errorMessage);
    return new Status(Status.ERROR, "PROVIDER_CALL_FAILED", "External provider returned an error");
  }

  return new Status(Status.OK);
};
```

#### Example: blocking error (throwing an exception)

When your hook throws an exception, the platform **blocks** the operation and rolls back the transaction. Use this only when the operation must not proceed.

```js
"use strict";

var Status = require("dw/system/Status");
var Logger = require("dw/system/Logger");

var log = Logger.getLogger("myapp", "hooks");

exports.commit = function (order) {
  var result = callExternalProvider(order);

  if (!result.success) {
    // Blocking: throw to prevent the operation from proceeding.
    log.error("Provider call failed for order {0}: {1}", order.orderNo, result.errorMessage);
    throw new Error("Provider call failed: " + result.errorMessage);
  }

  return new Status(Status.OK);
};
```

**Platform behavior:** The basket operation (or order creation, or cancellation) is rolled back. The shopper receives a 500 error response.

#### Implementation best practices

##### Service timeout handling

Extension point hooks run synchronously within the shopper request. If your hook calls an external API, the service timeout directly impacts shopper-facing latency. Use the Commerce Cloud Service Framework (`dw.svc`) with a service profile that defines explicit timeouts, rate limits, and circuit breakers.

**Recommended service profile settings for checkout-path hooks:**

```xml
<service-profile profile-id="myapp.provider.profile">
    <timeout-millis>5000</timeout-millis>
    <rate-limit-enabled>true</rate-limit-enabled>
    <rate-limit-calls>100</rate-limit-calls>
    <rate-limit-millis>1000</rate-limit-millis>
    <circuit-breaker-enabled>true</circuit-breaker-enabled>
    <circuit-breaker-max-calls>3</circuit-breaker-max-calls>
</service-profile>
```

- **`timeout-millis`:** Keep at 5000 ms or less for real-time checkout hooks. A 10-second timeout will cause unacceptable shopper latency.
- **Circuit breaker:** Enable with a low threshold (`max-calls: 3`) so the platform fails fast if your provider is down, rather than making shoppers wait through repeated timeouts.
- **Rate limiting:** Protect your provider from burst traffic during flash sales or load tests.

When a service call times out or the circuit breaker trips, the service framework returns a `Result` with `status !== 'OK'`. Handle this in your hook:

```js
var result = myService.call(requestPayload);

if (result.status !== "OK") {
  log.error("Service call failed: {0} (error: {1})", result.errorMessage, result.error);
  // Decide based on your extension point contract whether to throw or return Status.ERROR
}
```

##### Logging best practices

Use `dw.system.Logger` with a consistent category and prefix for all log messages. This makes it straightforward to filter your app's logs in Log Center.

```js
var Logger = require("dw/system/Logger");
var log = Logger.getLogger("myapp", "hooks");
```

- **`log.debug()`** — verbose details during development (request/response payloads)
- **`log.info()`** — key lifecycle events (hook invoked, service call succeeded)
- **`log.warn()`** — recoverable issues (returning `Status.ERROR`, fallback behavior)
- **`log.error()`** — unrecoverable failures (throwing an exception, data integrity issues)

Always include identifiers in log messages so issues can be traced:

```js
log.info(
  "Hook called for basket {0}, shipment count: {1}",
  lineItemCtnr.getUUID(),
  lineItemCtnr.getShipments().size(),
);

log.error("Hook failed for order {0}: {1}", order.orderNo, result.errorMessage);
```

> **Tip:** Enable `communication-log` on your service definition during development to capture full HTTP request/response details in Log Center. Disable it in production to avoid logging sensitive data.

##### Transaction ID tracking

If your external provider assigns a transaction ID, persist it on the order so subsequent hooks can reference the same transaction. Use order-level custom attributes for this:

```js
// In your first hook — store the transaction ID from your provider's response
var Transaction = require("dw/system/Transaction");

Transaction.wrap(function () {
  lineItemCtnr.custom.myAppTransactionId = providerResponse.transactionId;
});
```

```js
// In a later hook — reference the same transaction
var transactionId = order.custom.myAppTransactionId;
if (!transactionId) {
  log.warn("No transaction ID found on order {0}, skipping commit", order.orderNo);
  return new Status(Status.ERROR, "NO_TRANSACTION_ID", "Missing transaction ID");
}
callExternalProvider(transactionId, order);
```

Define the custom attribute in your IMPEX under `meta/system-objecttype-extensions.xml`:

```xml
<type-extension type-id="Order">
    <custom-attribute-definitions>
        <attribute-definition attribute-id="myAppTransactionId">
            <display-name xml:lang="x-default">Provider Transaction ID</display-name>
            <type>string</type>
            <mandatory-flag>false</mandatory-flag>
            <externally-managed-flag>true</externally-managed-flag>
        </attribute-definition>
    </custom-attribute-definitions>
</type-extension>
```

> **Naming convention:** Prefix all custom attribute IDs with your app name (for example, `myApp`) to avoid collisions with other Commerce Apps or merchant customizations.

##### Custom attribute usage patterns

Beyond transaction IDs, Commerce Apps commonly use custom attributes for:

- **Basket-level details:** Store intermediate calculation results so your hook can detect when a recalculation is actually needed (for example, by comparing a hash of line items and addresses).
- **Order-level audit data:** Store the provider's response code, document status, or error details for post-order reconciliation.
- **Site preferences:** Store merchant-configurable values like API mode (sandbox vs. production), default codes, or enable/disable flags.

Use `externally-managed-flag` set to `true` for attributes that should not appear in Business Manager's attribute editor (internal tracking fields). Set it to `false` for attributes merchants need to see or edit.

#### Optional: delivering Storefront Next components

Backend-only apps that implement extension points can optionally include Storefront Next extensions to provide custom UI beyond what the platform components offer. For example, Avalara provides an `avatax-tax-breakdown` extension that displays a detailed tax breakdown not covered by the standard tax line item component.

## Connection Health Check

Any Commerce App — UI-only, Backend-only, or Fullstack — can implement an optional **connection health check hook** so Business Manager displays connectivity status on the app's tile in the Cart & Checkout Hub. The platform calls the hook when a merchant loads the app details page or manually refreshes the connection status. How you define "connection" is up to you: an API ping, a credential validation, a third-party service check, or any verification that confirms your app's external dependency is reachable.

### Registering the hook

Add a `checkConnectionHealth` entry to your cartridge's `hooks.json`:

```json
{
  "hooks": [
    {
      "name": "sfcc.app.<domain>.checkConnectionHealth",
      "script": "./hooks/checkConnectionHealth.js"
    }
  ]
}
```

Replace `<domain>` with your app's domain (for example, `tax`, `shipping`, `payment`, `ratings-and-reviews`). The hook name must follow the `sfcc.app.<domain>.checkConnectionHealth` convention — the platform uses it to associate the hook with your app's installation.

> **Function naming convention:** The exported function must be named `checkConnectionHealth`, matching the last segment of the extension point name.

### Implementing the hook

The hook must export a `checkConnectionHealth` function that returns a `dw.system.Status`. Use `Status.OK` for healthy, `Status.ERROR` with code `DEGRADED` for partial impairment, and `Status.ERROR` with code `UNHEALTHY` (or any other error code) for failures.

```js
"use strict";

var Status = require("dw/system/Status");

exports.checkConnectionHealth = function () {
  var myService = require("*/cartridge/scripts/services/myService");

  try {
    var result = myService.call("GET", "/health", null);

    if (result.success) {
      var status = new Status(Status.OK, "HEALTHY");
      status.addDetail("message", "Service responded in " + result.latency + "ms");
      return status;
    }

    var unhealthy = new Status(Status.ERROR, "UNHEALTHY");
    unhealthy.addDetail("message", "Unable to reach service");
    unhealthy.addDetail(
      "remediation",
      "Verify credentials in Administration > Operations > Services > myvendor.api",
    );
    return unhealthy;
  } catch (e) {
    var errorStatus = new Status(Status.ERROR, "UNHEALTHY");
    errorStatus.addDetail("message", "Unexpected error: " + e.message);
    errorStatus.addDetail(
      "remediation",
      "Verify credentials in Administration > Operations > Services > myvendor.api",
    );
    return errorStatus;
  }
};
```

### Status codes

The Business Manager endpoint interprets the returned `Status` as follows:

| Return value | Health badge | When to use |
| :--- | :--- | :--- |
| `new Status(Status.OK, 'HEALTHY')` | **Healthy** | Service is reachable and credentials are valid |
| `new Status(Status.ERROR, 'DEGRADED')` | **Degraded** | Service is reachable but partially impaired (for example, auth failed, rate-limited) |
| `new Status(Status.ERROR, 'UNHEALTHY')` | **Unhealthy** | Service is unreachable or returning errors |
| `null` or exception thrown | **Unknown** | Hook timed out or threw — platform handles gracefully |

### Status details

Use `status.addDetail(key, value)` to attach structured information that Business Manager surfaces in the health indicator UI:

| Detail key | Purpose | Example |
| :--- | :--- | :--- |
| `"message"` | Brief description of the current state | `"Service responded in 142ms"` |
| `"remediation"` | Actionable steps the merchant can take to fix a degraded or unhealthy state | `"Verify your API credentials in Administration > Operations > Services > myvendor.api"` |

Business Manager surfaces both values **verbatim**. Keep them concise and merchant-friendly.

### Localizing health check messages

The hook executes in the Business Manager session locale context, so use `dw.web.Resource` to provide translated messages:

```js
var Resource = require("dw/web/Resource");
var Status = require("dw/system/Status");

exports.checkConnectionHealth = function () {
  // ... service call logic ...
  var degraded = new Status(Status.ERROR, "DEGRADED");
  degraded.addDetail("message", Resource.msg("healthcheck.degraded.message", "myapp", null));
  degraded.addDetail(
    "remediation",
    Resource.msgf("healthcheck.degraded.remediation", "myapp", null, serviceName),
  );
  return degraded;
};
```

Place resource bundles under `cartridge/templates/resources/` in your cartridge (for example, `myapp.properties` for English, `myapp_de.properties` for German). The `Resource.msg()` call resolves against the Business Manager admin's current language.

If you do not need localization, hard-coded English strings are acceptable — they flow through to the UI as-is.

### Best practices

- **Keep it lightweight.** The platform applies a CPU timeout. Prefer a simple ping or auth-check endpoint over heavy operations.
- **Always return a Status.** Never return `undefined` — return `null` if you cannot determine health, and the platform shows "Unknown."
- **Wrap in try/catch.** An unhandled exception produces an "Unknown" badge with no message. Catching the error lets you return a meaningful remediation hint.
- **Be specific in remediation.** Include the exact Business Manager navigation path where the merchant can fix the issue (for example, `Administration > Operations > Services > myvendor.api`).
- **Test degraded states.** Use invalid credentials or an unreachable endpoint in your sandbox to verify that degraded and unhealthy responses render correctly.

### Apps without a health check

If an app does not register `sfcc.app.<domain>.checkConnectionHealth`, the Cart & Checkout Hub tile does not display a health badge. The health check is optional — apps that don't depend on an external connection typically skip it.

## UI-Only and Fullstack Apps

If you want full control over the UX, you replace the platform component with your own React extension. For UI-only apps, the platform provides the backend. For fullstack apps, you also build your own backend.

### Building your Storefront Next extension

Your extension lives in the `storefront-next/src/extensions/{app-name}/` directory of your CAP. A typical extension includes:

- One or more React components
- A `target-config.json` that maps your components to UI target IDs and declares context providers
- Localization files under `locales/` (minimum: `en-US`, `en-GB`, `it-IT`)
- Optionally, route files under `routes/` for custom pages
- Optionally, a context provider component

**Storefront Next tech stack:**

| Layer        | Technology                                       |
| :----------- | :----------------------------------------------- |
| Framework    | React 19                                         |
| Language     | TypeScript (strict)                              |
| Build        | Vite                                             |
| Styling      | Tailwind CSS 4 (`@theme inline`, no config file) |
| Components   | ShadCN UI (Radix UI primitives)                  |
| Variants     | CVA (class-variance-authority)                   |
| Routing      | React Router 7                                   |
| i18n         | react-i18next                                    |
| Unit testing | Vitest + React Testing Library                   |
| E2E testing  | CodeceptJS + Playwright                          |

Example extension directory structure:

```
src/extensions/my-app/
  target-config.json
  components/
    my-widget.tsx
    my-other-widget.tsx
  locales/
    en-US/translations.json
    en-GB/translations.json
    it-IT/translations.json
  providers/
    my-app-provider.tsx        # optional context provider
  routes/
    _app.my-app-page.tsx       # optional custom route
  hooks/
    use-my-app.ts              # optional custom hooks
```

#### target-config.json

The `target-config.json` defines which platform UI targets your components replace or extend, and optionally declares context providers that wrap the application root.

**Components** map to `UITarget` placeholders defined throughout the Storefront Next application. Each `UITarget` has a unique `targetId` — your extension specifies which target IDs to fill, the component file path (relative to `src/`), and an `order` value for controlling render order when multiple extensions target the same ID.

Target IDs follow the naming convention `sfcc.{location}.{domain}.{capability}` — for example, `sfcc.orderSummary.tax.line` targets the tax line item within the order summary, and `sfcc.pdp.reviews.rating` targets the star rating display on the product detail page.

```json
{
  "components": [
    {
      "targetId": "sfcc.orderSummary.tax.line",
      "path": "extensions/avatax-tax-breakdown/components/tax-line.tsx",
      "order": 0
    }
  ]
}
```

Multiple components can target different insertion points in the same `target-config.json`. For example, a Ratings & Reviews extension might target the star rating, the full reviews list, and the Q&A section on the PDP:

```json
{
  "components": [
    {
      "targetId": "sfcc.pdp.reviews.rating",
      "path": "extensions/yotpo-reviews/components/star-rating.tsx",
      "order": 0
    },
    {
      "targetId": "sfcc.pdp.reviews.list",
      "path": "extensions/yotpo-reviews/components/reviews-list.tsx",
      "order": 0
    },
    {
      "targetId": "sfcc.pdp.reviews.qna",
      "path": "extensions/yotpo-reviews/components/qna-section.tsx",
      "order": 0
    }
  ]
}
```

When multiple components target the same `targetId`, they are rendered in ascending `order`.

**Context providers** let your extension inject a React context provider at the application root (wrapping `root.tsx`). This is useful when your extension needs shared state accessible across multiple components or routes:

```json
{
  "components": [
    {
      "targetId": "sfcc.checkout.payment.paymentMethods",
      "path": "extensions/my-payments/components/payment-methods.tsx",
      "order": 0
    }
  ],
  "contextProviders": [
    {
      "path": "extensions/my-payments/providers/payments-provider.tsx",
      "order": 0
    }
  ]
}
```

**Available UI target IDs** are defined throughout the Storefront Next application using the `<UITarget targetId="..." />` component. Each target ID corresponds to a specific domain and location in the storefront. Consult the Storefront Next template application for the full list of available target IDs.

#### How UITarget replacement works at build time

`UITarget` is a **build-time placeholder**, not a runtime component. When the merchant runs `pnpm dev` or `pnpm build`, a Vite plugin scans all `target-config.json` files under `src/extensions/`, builds a component registry, and uses Babel AST transforms to:

1. **Replace** each `<UITarget targetId="X">` with the registered extension component(s) for that target ID, generating the necessary `import` statements automatically.
2. **Preserve children** if no extension targets that ID — `<UITarget targetId="X">{defaultContent}</UITarget>` renders `defaultContent`.
3. **Remove** `<UITarget>` entirely if no extension targets it and no children are provided.

Context providers declared in `contextProviders` are similarly injected at build time by replacing the `<UITargetProviders>` placeholder in `root.tsx` with nested provider wrappers.

> **Important:** Changes to `target-config.json` during development automatically restart the Vite dev server so the registry is rebuilt.

#### Extension routes

Extensions can add custom pages by placing route files in a `routes/` subdirectory. These follow the same file-based routing conventions as the main application (React Router v7 flat routes). Route files are automatically discovered and merged into the application's route tree at build time — no manual route registration is needed.

```
src/extensions/my-app/
  routes/
    _app.my-app-settings.tsx    # page route under the _app layout
    action.my-app-action.ts     # resource route (action endpoint)
    resource.my-app-data.ts     # resource route (data endpoint)
```

#### Extension localization

Extensions must include translation files for at least `en-US`, `en-GB`, and `it-IT` locales. Place them at `locales/{locale}/translations.json` within your extension directory.

Translation namespaces are automatically generated from the extension folder name using PascalCase with an `ext` prefix. For example, an extension in `src/extensions/my-app/` gets the namespace `extMyApp`.

```typescript
import { useTranslation } from "react-i18next";

export function MyAppWidget() {
  const { t } = useTranslation("extMyApp");
  return <h1>{t("widget.title")}</h1>;
}
```

An aggregation script (`aggregate-extension-locales`) runs during the build process to discover extension locale files and generate import manifests. This is handled automatically — you don't need to manually register your translations.

#### Extension installation

During installation, the extension code from your CAP is copied into the merchant's Storefront Next project under `src/extensions/{app-name}/`.

On the next build, the Vite plugin reads the new `target-config.json`, rebuilds the component registry, and replaces `UITarget` placeholders with your components.

### Injecting External SDK Scripts

Many Commerce Apps need to load an external JavaScript library from a vendor's CDN — fraud beacons, analytics tags, payment SDKs, review widgets, live chat, and more. This section explains how to inject those scripts using the existing Storefront Next extension mechanisms (`contextProviders` and `components`). No framework changes are required.

> **Important — CDN delivery is required by most vendors.** Many payment and fraud vendors' terms of service require loading from their CDN. Fraud detection vendors use CDN delivery as part of integrity verification. Personalization tools need CDN delivery for real-time experiment configurations. Bundling these scripts would add hundreds of KB to the storefront and freeze vendors at specific versions. Always load external SDKs via `<script src="https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/...">` rather than bundling them.

#### How script injection works in Storefront Next

Storefront Next is a Single Page Application (SPA) built on React 19. Two properties of this architecture are critical for script injection:

1. **React 19 script hoisting:** Any `<script src="https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/...">` tag rendered anywhere in the React component tree is automatically hoisted to `<head>` and deduplicated by `src`. This means a `contextProvider` or component that renders a `<script>` tag gets that script in `<head>` regardless of where the component appears in the tree. The `async` and `defer` attributes pass through correctly.

2. **SPA navigation:** Unlike traditional multi-page sites, Storefront Next navigates between pages by swapping content in place — no full page reload occurs. SDK scripts that rely on page loads to re-initialize (review widgets, fraud beacons, analytics) must be explicitly notified of route changes. Use the `useLocation()` hook from `react-router` to detect SPA navigation and re-trigger SDK logic.

> **Note:** Inline `<script>` blocks (containing code rather than a `src` URL) are **not** hoisted by React 19. Use `useEffect` for any `window` object initialization logic instead.

#### Choosing the right mechanism

| Scenario                                                                             | Mechanism                                                                                        |
| :----------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------- |
| SDK must load on every page (fraud, analytics, tag manager, chat)                    | `contextProviders` — wraps the entire app, loads once                                            |
| SDK only needed on specific pages (payment SDK at checkout)                          | `components` at a scoped UI target (for example, `sfcc.checkout.page.before`)                    |
| SDK needed on multiple page types (BNPL messaging on PDP + payment form at checkout) | `contextProviders` (loads SDK globally) + `components` at each target (renders UI)               |
| Widget requires DOM container + SPA re-initialization                                | `contextProviders` (loads SDK) + `components` (renders container, calls re-init on route change) |

#### Example: Global SDK via contextProvider

SDKs that must load on every page — fraud beacons, analytics, tag managers, live chat — use a `contextProvider` that renders a `<script>` tag and wraps the entire application. The provider reads merchant configuration from `useConfig()` and uses `useLocation()` to detect SPA navigation.

**target-config.json:**

```json
{
  "components": [],
  "contextProviders": [
    {
      "path": "extensions/my-fraud-app/providers/FraudBeaconProvider.tsx",
      "order": 0
    }
  ]
}
```

**FraudBeaconProvider.tsx:**

```tsx
'use client';
import { useEffect, useState, type ReactNode, type ReactElement } from 'react';
import { useLocation } from 'react-router';
import { useConfig } from '@salesforce/storefront-next-runtime/config';

interface AppConfig {
  extension?: { myFraudApp?: { siteId: string; enabled: boolean } };
}

export default function FraudBeaconProvider({ children }: { children: ReactNode }): ReactElement {
  const appConfig = useConfig<AppConfig>();
  const siteId = appConfig.extension?.myFraudApp?.siteId ?? '';
  const enabled = appConfig.extension?.myFraudApp?.enabled !== false;
  const location = useLocation();

  useEffect(() => {
    if (!enabled || !siteId) return;
    // Re-notify the vendor SDK on each SPA navigation.
    // Replace with your vendor's page-notify API.
    const win = window as Window & { vendorSdk?: { notifyPageView?: (id: string) => void } };
    win.vendorSdk?.notifyPageView?.(siteId);
  }, [enabled, siteId, location.pathname]);

  return (
    <>
      {enabled && siteId && (
        <script src="https://cdn.example.com/fraud-beacon.js" data-site-id={siteId} />
      )}
      {children}
    </>
  );
}
```

- No `async` attribute — fraud beacons must load near-synchronously to capture session start. For analytics or tag manager scripts that don't need to block rendering, add `async`.
- `useLocation()` triggers the `useEffect` on every SPA navigation so the vendor SDK is notified of route changes.
- Configuration comes from `useConfig()`, which reads values the merchant sets during app configuration.

#### Example: Checkout-scoped SDK via components

SDKs needed only on specific pages — payment processors at checkout — use `components` at scoped UI targets instead of `contextProviders`. The script loads only while the target's route is active.

**target-config.json:**

```json
{
  "components": [
    {
      "targetId": "sfcc.checkout.page.before",
      "path": "extensions/my-payment/components/PaymentScriptLoader.tsx",
      "order": 0
    },
    {
      "targetId": "sfcc.checkout.payment.paymentMethods",
      "path": "extensions/my-payment/components/PaymentForm.tsx",
      "order": 0
    }
  ]
}
```

**PaymentScriptLoader.tsx:**

```tsx
export default function PaymentScriptLoader() {
  return <script src="https://cdn.example.com/payment-sdk.js" async />;
}
```

- `sfcc.checkout.page.before` renders only on the checkout route. React 19 hoists the `<script>` to `<head>` only while that route is active.
- `sfcc.checkout.payment.paymentMethods` is a wrap-behavior target that replaces the default payment method UI.

#### Handling SPA re-initialization

Widget SDKs (common in ratings & reviews) that scan the DOM for containers on initialization don't know when React mounts new content during SPA navigation. For these, use both mechanisms: a `contextProvider` loads the SDK globally and exposes a `refreshWidgets()` function via React context, and a `component` renders the DOM container and calls `refreshWidgets()` in a `useEffect` that watches `useLocation().pathname`. This triggers the SDK to re-scan the DOM after each route change.

#### Script placement decision guide

| Question                                   | Mechanism                                                                    |
| :----------------------------------------- | :--------------------------------------------------------------------------- |
| Must run on every page from session start? | `contextProviders` — no `async` (fraud beacons)                              |
| Must run on every page, non-blocking?      | `contextProviders` — with `async` (analytics, tag managers)                  |
| Must run before React renders?             | Not possible today — requires future framework update                        |
| Only needed on checkout?                   | `components` at `sfcc.checkout.page.before` (payment SDKs)                   |
| Needs DOM container + SPA re-init?         | `contextProviders` (SDK) + `components` (container + `refreshWidgets()`)     |
| Same SDK serves multiple page types?       | `contextProviders` (SDK) + `components` at each target (BNPL)                |
| Needs customer identity after auth?        | `contextProviders`, identity update in `useEffect` watching auth state       |
| Manages other vendor scripts?              | `contextProviders` with `dataLayer`, virtual page views on route change      |
| Blocks or gates other scripts on consent?  | `contextProviders` at low `order`, others check consent hook — partial today |

### Storefront Action Hooks

While UI targets and context providers handle the client side, many Commerce App domains also need to run server-side logic at key points in the shopper experience — verifying a shipping address against an external service, checking a fraud score before placing an order, or authorizing a payment. Storefront action hooks provide these server-side extension points within the Storefront Next application.

Action hooks run inside the Storefront Next server as part of React Router actions (not in the Commerce Cloud Script API like backend extension points such as `sfcc.app.tax.*`). They are declared in `target-config.json` under the `actionHooks` key and processed at build time by a Vite plugin. At runtime, hooks execute in a **waterfall** pattern: each handler receives the previous handler's output, with a **5-second timeout** per handler.

The hooks available today cover the checkout flow. Additional action hooks for other storefront surfaces will be added in future releases.

#### Available action hooks

| Hook ID                                                        | Blocking | Surface  | Purpose                                                          |
| :------------------------------------------------------------- | :------- | :------- | :--------------------------------------------------------------- |
| `sfcc.checkout.fraud.afterSubmitContactInfo`                   | No       | Checkout | Fraud or identity checks after email and phone submission        |
| `sfcc.checkout.addressVerification.afterSubmitShippingAddress` | No       | Checkout | Address validation and standardization                           |
| `sfcc.checkout.shipping.afterMethodsFetch`                     | No       | Checkout | Enrich or filter shipping methods (for example, real-time rates) |
| `sfcc.checkout.shipping.afterMethodSelect`                     | No       | Checkout | Post-processing after shipping method selection                  |
| `sfcc.checkout.payments.afterSubmitPayment`                    | No       | Checkout | Post-payment processing (for example, tokenization)              |
| `sfcc.checkout.fraud.beforePlace`                              | **Yes**  | Checkout | Final fraud gate — can block order creation                      |
| `sfcc.checkout.payments.beforePlaceOrder`                      | **Yes**  | Checkout | Payment authorization gate — can block order creation            |
| `sfcc.checkout.payments.afterPlaceOrder`                       | No       | Checkout | Post-order processing (for example, payment capture)             |

**Blocking** hooks abort the action on any handler failure. **Non-blocking** hooks log errors and continue to the next handler. Throwing an `ActionHookError` always aborts the action with a user-facing error, regardless of blocking mode.

#### Declaring and writing action hooks

Add an `actionHooks` array to your `target-config.json`. Each entry specifies the hook ID, the handler file path (relative to `src/`), and an `order` value. When multiple extensions target the same hook, handlers execute in ascending `order`.

```json
{
  "actionHooks": [
    {
      "hookId": "sfcc.checkout.addressVerification.afterSubmitShippingAddress",
      "handler": "extensions/my-address-app/hooks/verify-address.server.ts",
      "order": 0
    }
  ]
}
```

A handler is a default-exported async function that receives an `ActionHookContext` (containing `data` from the checkout step and the React Router `actionContext`) and returns a modified context, or `void` to pass through unchanged.

```typescript
import type { ActionHookContext } from "@/targets/action-hook.server";
import { ActionHookError } from "@/targets/action-hook.server";

export default async function verifyAddress(
  context: ActionHookContext,
): Promise<ActionHookContext> {
  const { data } = context;

  const response = await fetch("https://api.example.com/address/verify", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ address: data.shippingAddress }),
  });
  const verified = await response.json();

  if (!verified.valid) {
    throw new ActionHookError(
      "Please check your shipping address and try again.",
      "sfcc.checkout.addressVerification.afterSubmitShippingAddress",
      "shippingAddress",
    );
  }

  return { ...context, data: { ...data, shippingAddress: verified.standardizedAddress } };
}
```

`ActionHookError` returns a 400 JSON response to the storefront. The third argument (`step`) identifies the checkout step where the error should display.

#### Listing available extension points

The Storefront Next template includes a script that enumerates all UI targets and action hooks:

```bash
pnpm run list:extension-points
```

This outputs a grouped table of every UI target ID (with its source file) and every action hook ID (with its blocking status and invocation site). Pass `--json` for machine-readable output.

### Building the back-end

Fullstack apps have two sub-types: **Extension Points** and **Custom SCAPI**. See [The Three Architectures](/docs/commerce/b2c-commerce/guide/architecture.md#the-three-architectures-for-commerce-apps) for an overview of each.

#### Extension Points

For fullstack apps using extension points, your backend follows the same patterns as backend-only apps (hook implementations, service integrations, IMPEX). The difference is that you also provide your own Storefront Next extensions that replace or extend the platform's default UI components.

#### Custom SCAPI

For domains without platform-defined extension points, you build a custom SCAPI that supplies data to your own Storefront Next components. You define the data contract between your components and your SCAPI.

Examine the mocked component in the Storefront Next reference application (Market Street or Salesforce Foundations) to understand what data shape to target. Build your custom SCAPI to return data in a format your components can consume.

**Reference:** SCAPI documentation for building custom API endpoints is maintained in [B2C API Documentation](https://developer.salesforce.com/docs/commerce/commerce-api/guide/scapi-get-started.html). This guide provides the Commerce App context; refer to the SCAPI docs for the implementation details.

## Backend Development Notes

Backend cartridges use the Commerce Cloud Script API (CommonJS modules with `require('dw/...')` imports). Key points for Commerce App backend development:

- **Service framework:** Use the Service Framework (`dw.svc`) for all outbound API calls. Define service credentials, profiles, and definitions in your IMPEX. Use placeholder credentials — merchants configure real values during setup.
- **Hook registration:** Register extension point hooks in `cartridge/scripts/hooks.json`. The platform resolves hooks through the Extension Registry, not cartridge path scanning.
- **Feature toggles:** Enable required prerequisite toggles before installation. When the supplied app ID and domain match a registry entry, the installer checks `requiredFeatureToggle` when that entry defines it, but doesn't enable the toggle. Hook validation can also identify disabled hook prerequisites that require remediation.
- **Error handling:** Extension point hooks have defined error semantics per hook. Review the hook contract carefully — some hooks block on errors while others log warnings and proceed. See [Extension Points](#extension-points) for per-hook details.

## Example Apps in the Registry

The [Commerce App Registry](https://github.com/SalesforceCommerceCloud/commerce-apps) contains reference implementations to study when building your own app. These aren't open source — they're packaged CAPs available through the registry — but their structure, IMPEX patterns, and `commerce-app.json` files serve as practical templates.

| App         | Domain | Architecture                                                         | What It Demonstrates                                                                                              |
| :---------- | :----- | :------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------- |
| Avalara Tax | `tax`  | Backend-only (extension points) + optional Storefront Next extension | Extension point hooks (`calculate`, `commit`, `cancel`), service integration, `avatax-tax-breakdown` UI extension |

> **Tip:** Use the `/scaffold-app` Claude Code skill to generate a starter app structure modeled after these reference apps. The scaffold includes all required files and follows the same conventions.

### Anti-patterns to avoid

- **Hardcoded site IDs in IMPEX.** Use `SITEID` and `LIBRARYID` placeholders. The install job replaces them at runtime. Hardcoded values cause failures on any site other than the one you tested with.
- **Skipping uninstall IMPEX.** Every resource your app creates (services, custom objects, site preferences) must have a corresponding uninstall entry with `mode="delete"`. Missing uninstall IMPEX leaves orphaned configuration on the merchant's instance.
- **Omitting the service profile.** Always define a service profile with explicit `timeout-millis` and circuit breaker settings. Without a profile, service calls use platform defaults that may be too generous for checkout-path hooks.
- **Generic custom attribute names.** Prefix all custom attribute IDs with your app name (for example, `avalaraTaxDocumentId`, not `taxDocumentId`). Generic names risk collisions with other Commerce Apps or merchant customizations.
- **Bundling SiteGenesis or SFRA cartridges.** Commerce Apps are Storefront Next-only. Including legacy storefront cartridges in your CAP will cause conflicts and is a review rejection reason.
