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.

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.

Backend-only apps implement platform-defined extension point hooks. See The Three Architectures for an overview.

If your domain has a platform-defined extension point (currently Tax, with additional domains later in 2026), 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 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.

Example hooks.json structure:

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.

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.

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.

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.

Platform behavior: The basket operation (or order creation, or cancellation) is rolled back. The shopper receives a 500 error response.

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:

  • 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:

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.

  • 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:

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:

Define the custom attribute in your IMPEX under meta/system-objecttype-extensions.xml:

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.

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.

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.

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:

LayerTechnology
FrameworkReact 19
LanguageTypeScript (strict)
BuildVite
StylingTailwind CSS 4 (@theme inline, no config file)
ComponentsShadCN UI (Radix UI primitives)
VariantsCVA (class-variance-authority)
RoutingReact Router 7
i18nreact-i18next
Unit testingVitest + React Testing Library
E2E testingCodeceptJS + Playwright

Example extension directory structure:

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.

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:

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:

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.

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.

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.

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.

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.

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.

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="..."> rather than bundling them.

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="..."> 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.

ScenarioMechanism
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-initializationcontextProviders (loads SDK) + components (renders container, calls re-init on route change)

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:

FraudBeaconProvider.tsx:

  • 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.

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:

PaymentScriptLoader.tsx:

  • 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.

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.

QuestionMechanism
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

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.

Hook IDBlockingSurfacePurpose
sfcc.checkout.fraud.afterSubmitContactInfoNoCheckoutFraud or identity checks after email and phone submission
sfcc.checkout.addressVerification.afterSubmitShippingAddressNoCheckoutAddress validation and standardization
sfcc.checkout.shipping.afterMethodsFetchNoCheckoutEnrich or filter shipping methods (for example, real-time rates)
sfcc.checkout.shipping.afterMethodSelectNoCheckoutPost-processing after shipping method selection
sfcc.checkout.payments.afterSubmitPaymentNoCheckoutPost-payment processing (for example, tokenization)
sfcc.checkout.fraud.beforePlaceYesCheckoutFinal fraud gate — can block order creation
sfcc.checkout.payments.beforePlaceOrderYesCheckoutPayment authorization gate — can block order creation
sfcc.checkout.payments.afterPlaceOrderNoCheckoutPost-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.

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.

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.

ActionHookError returns a 400 JSON response to the storefront. The third argument (step) identifies the checkout step where the error should display.

The Storefront Next template includes a script that enumerates all UI targets and action hooks:

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.

Fullstack apps have two sub-types: Extension Points and Custom SCAPI. See The Three Architectures for an overview of each.

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.

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. This guide provides the Commerce App context; refer to the SCAPI docs for the implementation details.

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: The platform automatically enables required feature toggles during installation (for example, TaxAppHooksEnabled for tax apps). You don’t need to manage these manually. See Feature Toggles for details.
  • 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 for per-hook details.

The Commerce App Registry 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.

AppDomainArchitectureWhat It Demonstrates
Avalara TaxtaxBackend-only (extension points) + optional Storefront Next extensionExtension 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.

  • 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.