Composable Storefront on CCv2: Build, Runtime Configuration, and OCC Connectivity
Deploy Composable Storefront on CCv2 with reproducible artifacts, runtime OCC configuration, CORS, private API access, and browser/SSR verification.
Priya Ramanathan
SAP Commerce Storefront & Frontend Expert
Composable Storefront and Spartacus, SSR, OCC optimization, and Angular architecture.
Composable Storefront on SAP Commerce Cloud is two deployable concerns joined by an API contract: the Commerce application exposes OCC, while the JavaScript storefront application is built and served by the JavaScript storefront services, optionally with Node SSR. Most failed deployments are not Angular defects. They are mismatches between the artifact that CCv2 discovers, the runtime endpoint it injects, the proxy/CORS policy that receives the call, and the configuration that the browser and SSR process actually use.
This guide is scoped to Commerce Cloud 2211-style JavaScript storefront deployment. Cloud Portal schema, supported Node lines, and library package paths change over time; treat examples as a verification model, not a substitute for the exact environment documentation. It intentionally does not repeat general CCv2 web/network architecture or basic OCC contract ownership.
Model the deployment as four contracts#
When a deployment fails, identify which contract is broken before changing anything.
| Contract | Owns | Failure signature | Evidence that proves it |
|---|---|---|---|
| Source/build | Repository layout, manifest application entry, lock file, scripts, Node compatibility | Build fails, or the wrong/stale artifact is deployed | Build log plus artifact tree under the configured application path. |
| Runtime endpoint | index.html placeholder, API endpoint selection, per-app service properties | Browser calls old/wrong host, SSR works differently from CSR, blue/green drift | Delivered index.html, browser Network URL, JS App and JsApp-SSR service config. |
| Transport/security | DNS, TLS, proxy, CORS, IP/API filtering, headers, frame policy | Preflight failure, 400/403, browser-only failure, SmartEdit iframe blocked | Raw OPTIONS/GET responses and proxy/API policy evidence. |
| Storefront behavior | provideConfig, SSR server configuration, base site/context values | Correct endpoint reaches OCC but wrong site/language/data is rendered | Effective config plus representative page/OCC trace. |
The contracts are deliberately separate. A browser CORS message does not prove CORS is the cause; a 400 response can be a proxy/header-size issue. A valid browser base URL does not prove Node SSR uses the same endpoint. A successful build does not prove CCv2 built the source you expected.
Make the JavaScript application discoverable and reproducible#
The JavaScript storefront manifest declares applications under js-storefront/. SAP's Add Applications to JavaScript Storefronts document is the source of truth for the current manifest schema. This is the relevant shape:
{
"applications": [
{
"name": "storefront",
"path": "storefront",
"enabledRepositories": ["spartacus-6-cdn"]
}
]
}
Do not infer the repository name from the storefront version. SAP's 2211 documentation specifies the repository enablement separately from the particular 5.x, 6.x, or 2211.x package version installed by the project. Keep the exact registry/RBSC configuration and lock-file policy in source control; uncontrolled lock-file regeneration is a dependency upgrade disguised as a deployment fix.
The name/path relationship is operationally significant:
repository root/
└── js-storefront/
├── manifest.json
└── storefront/ # application.path
├── package.json
├── yarn.lock or package-lock.json
├── src/
└── dist/
└── storefront/ # dist/<application.name>
├── index.html
├── browser/ ...
└── server/ ... # when SSR build emits it
For a source application, the platform expects the application package.json, lock file, and source under application.path. For a prebuilt application, the output—including index.html—must be under application.path/dist/application.name.
There is a sharp edge: for each manifest application, CCv2 checks whether application.path/dist already exists. If it does not, it installs dependencies and runs yarn build for CSR or yarn build:ssr for SSR. If it does exist, the platform assumes it contains a prebuilt artifact and skips that build step. A stale checked-in dist/ directory can therefore deploy an old bundle while the commit looks correct.
# Run in CI or locally before creating a build; do not delete artifacts blindly in a shared branch.
test -f js-storefront/storefront/package.json
test -f js-storefront/storefront/yarn.lock || test -f js-storefront/storefront/package-lock.json
test -f js-storefront/storefront/src/index.html
# If this project intentionally commits a prebuilt artifact, validate its exact discovery path.
test -f js-storefront/storefront/dist/storefront/index.html
Decide explicitly which of these two models the repository uses:
| Model | Repository contains | CCv2 behavior | Required release control |
|---|---|---|---|
| Source build | Source, manifest, package manifest, reproducible lock file; no accidental dist | Installs dependencies and invokes the declared build script | Build locally/CI with the same supported Node line and preserve lock-file integrity. |
| Prebuilt artifact | dist/<application.name> with the complete browser/SSR output | Treats dist as authoritative and skips the build | Generate artifact in controlled CI, publish exactly one build output, verify its commit/version marker. |
Never mix them accidentally. A failing yarn build:ssr should not be “fixed” by committing whatever local dist happened to exist.
Diagnose build failures by their actual contract#
| Build log symptom | Check first | Corrective direction |
|---|---|---|
package.json / lock file not found | application.path, case sensitivity, manifest path | Fix the manifest/repository layout; do not alter Angular code. |
yarn build works but SSR deploy fails | build:ssr script, server output, Node runtime compatibility | Run the actual SSR script locally using the target support matrix. |
| Missing Composable package or unauthorized registry | .npmrc/.yarnrc, RBSC configuration, entitlement | Restore the approved SAP registry configuration; regenerate lock files only as a controlled dependency change. |
| Build skipped unexpectedly | Existing application.path/dist | Remove/replace stale prebuilt output through the project’s intended artifact workflow. |
| Node engine/module syntax error | engines, Angular/CLI/build tool compatibility, Cloud Portal supported Node versions | Select a Node line supported by both the deployed Commerce Cloud environment and the installed Angular/Composable release. |
manifest.json / server main / SSR package cannot be resolved | Built artifact tree and the server.ts entry expected by the version | Repair the application build contract; do not solve it through runtime service properties. |
SAP has withdrawn older Node lines over time; KBA 3363029 is a useful reminder that a locally supported Node release can be removed from the platform. Do not pin an example Node major from an old blog. Verify the current Cloud Portal options and the package manager engines constraints for the exact storefront release.
Configure OCC at runtime, once and deliberately#
The runtime base URL is not a cosmetic Angular setting. It determines where every browser and server-side OCC request goes and whether blue/green routing can work correctly.
For a JavaScript storefront application, use SAP's documented placeholder in the application index.html:
<!-- src/index.html or the generated prebuilt index.html -->
<meta name="occ-backend-base-url" content="OCC_BACKEND_BASE_URL_VALUE" />
At startup, the platform replaces this placeholder with the first API endpoint it created for the application. SAP's base-URL configuration documentation explains the precedence rule that catches many teams: backend.occ.baseUrl supplied through provideConfig() takes precedence over this meta tag. If you expect runtime injection, remove the competing hard-coded baseUrl; otherwise the document can contain the correct platform value while the application continues calling the old host.
// Do not keep this hard-coded alongside the runtime meta tag.
provideConfig({
backend: {
occ: {
baseUrl: 'https://old-api.example.invalid',
},
},
});
The placeholder is intentionally ignored when it is empty or still contains OCC_BACKEND_BASE_URL_VALUE outside the platform replacement flow. Validate the served HTML, not only source control:
curl --silent --show-error --fail \
'https://storefront.example.test/' \
| grep -oE '<meta name="occ-backend-base-url" content="[^"]+"'
Decide endpoint selection per application#
There are two supported deployment shapes, and they serve different goals.
| Need | Configuration | Trade-off |
|---|---|---|
| Standard platform routing / blue-green-friendly endpoint selection | Keep OCC_BACKEND_BASE_URL_VALUE in index.html; let the platform inject the appropriate API endpoint | Browser and app URL must be valid in each target environment. |
| Explicit backend per named JS App | Set ccv2.occ.backend.base.url.<jsapp>.value in JavaScript storefront service configuration | You own endpoint drift and must configure it in both JS App and JsApp-SSR when SSR is enabled. |
| Canary deployment | Keep the documented OCC_BACKEND_BASE_URL_VALUE placeholder | SAP warns not to hard-code the endpoint or use the explicit per-app property for the canary routing case. |
For example, an explicitly pinned application named storefront uses this service property name:
ccv2.occ.backend.base.url.storefront.value=https://api.example.test
If SSR is enabled, set that same per-app property in the JavaScript App service and the JsApp-SSR service configuration. A browser-only setting produces an easy-to-miss split: CSR goes to the intended API while Node SSR may use a different/default endpoint or fail authorization. SAP documents the property and dual-service requirement in Add Applications to JavaScript Storefronts.
Treat service-property values as environment configuration, not as source code. Do not put passwords, OAuth client secrets, API keys, or user-specific values into index.html, runtime metadata, or a client-side config service. Anything delivered to the browser is public.
Prove that browser and SSR call the same intended backend#
Create a small endpoint matrix for each deployment. The exact header names and gateway behavior are environment-specific, so compare URLs/statuses and approved trace IDs rather than assuming one technology produces the same headers everywhere.
| Caller | How to prove | Expected result |
|---|---|---|
| Browser CSR | DevTools Network on initial hydration and later navigation | OCC requests use the injected/pinned API host and correct base site. |
| SSR Node | Initial HTML request plus Node/OCC trace correlation | Server-initiated OCC requests reach the same intended API policy/region. |
| Direct public API test | Authorized curl to a public OCC resource | Returns expected CORS/security response without credentials being exposed. |
| SmartEdit preview | Open preview route inside SmartEdit | Correct page data and no iframe/frame-policy violation. |
| Blue/green or canary | Deployment preview/canary endpoint test | The document placeholder resolves to the deployment-appropriate API, not a hard-coded old environment. |
# CORS-independent API reachability probe. Use a public, non-mutating resource.
curl --silent --show-error --fail --include \
'https://api.example.test/occ/v2/electronics-spa/languages'
# Browser preflight probe: origin must be an allowed storefront origin.
curl --silent --show-error --include --request OPTIONS \
-H 'Origin: https://storefront.example.test' \
-H 'Access-Control-Request-Method: GET' \
-H 'Access-Control-Request-Headers: authorization,content-type' \
'https://api.example.test/occ/v2/electronics-spa/languages'
Keep access-token and cookie headers out of screenshots, logs, issue attachments, and shell history. A 401/403 from this anonymous probe may be correct for a protected resource; select a public endpoint for the test.
Configure CORS as an exact origin policy#
Composable Storefront can be on a different origin from OCC. Browsers enforce CORS; Node SSR does not enforce browser CORS, which is why a server-rendered page can appear healthy while browser interactions fail.
| Browser observation | Likely cause | Evidence to collect | Safe change |
|---|---|---|---|
Preflight has no Access-Control-Allow-Origin | Origin missing from Commerce CORS configuration | Raw OPTIONS request with Origin | Add the precise storefront origin. |
| Credentials request with wildcard origin | Access-Control-Allow-Credentials: true cannot be combined with * origin | Browser request mode and response headers | Use explicit allowed origin(s), never a wildcard. |
CORS error plus HTTP 400 | Often a rejected/oversized request header, not CORS | Proxy/API log and response status | Diagnose header size/cookie/token bloat before changing CORS. |
| CSR fails but SSR initially works | Browser is blocked by CORS while Node reaches OCC | Side-by-side SSR and browser request traces | Correct origin/method/header policy. |
| Only one site/language fails | Base URL/site context or CDN origin variation | Effective URL, host, Origin, base-site parameter | Fix the configuration variant, not a global rule. |
A valid credentialed response has a concrete shape:
Access-Control-Allow-Origin: https://storefront.example.test
Access-Control-Allow-Credentials: true
Vary: Origin
Do not answer a CORS failure by adding * everywhere. SAP KBAs 3089617 and 3125076 cover Commerce CORS behavior, including the restriction on wildcard origins with credentials. KBA 3278460 covers a useful counterexample: a front-end “CORS” symptom can be a 400 response caused by headers that are too large.
Handle SSR 403s as an API policy problem#
If SSR receives a global “not authorized” error but client-side browser requests work, do not open a protected endpoint to the world. The Node SSR request originates from the JavaScript application infrastructure and can be blocked by Commerce API filters, CDN security, or IP restrictions even when a customer browser is allowed.
Use the following escalation packet:
- Route, timestamp, base site, API host, and Node/Commerce correlation ID.
- Exact HTTP status and sanitized response body; no cookie/token headers.
- Proof whether the call uses the expected public API endpoint or an explicitly configured private endpoint.
- Existing API filter/allow-list policy and the intended minimum source CIDR/service identity.
- Result of the same route from the browser and from SSR.
KBA 3103064 describes the private-endpoint filtering case. The safe resolution is to allow the approved SSR source through the private API policy with the platform/SAP support process, not to switch a restricted API to Deny None or make it public. The SSR optimization reference also calls out API/CDN allow-list checks for server-rendered global authorization errors.
Keep SmartEdit and external domains in the release checklist#
SmartEdit uses an iframe/preview context. A storefront response that sends X-Frame-Options: DENY can work for shoppers but fail in SmartEdit. KBA 3334671 documents that failure mode. Frame policy is a security control, so scope any exception to the required preview integration; do not relax clickjacking protections globally because an editor needs a page rendered.
External storefront domains add DNS/TLS/origin, CORS, cookie, and redirect variants. Test them as first-class production routes:
https://www.brand.example -> public shopper origin
https://preview.brand.example -> authorized preview/SmartEdit origin if required
https://api.brand.example -> public API origin selected by JS App runtime config
For each host, test an anonymous PDP/PLP, an account/checkout route, one CMS change/preview workflow, CSP/frame headers, cookie SameSite behavior, and the exact CORS Origin. Do not assume a configuration that works through the platform’s default domain transfers unchanged to a vanity domain.
Release workflow: verify the deployed artifact, not the intent#
Use this deployment gate before promoting an environment:
| Step | Verification | Failure it prevents |
|---|---|---|
| 1. Inspect source contract | Manifest name/path, source-vs-prebuilt choice, package manager lock file, build/build:ssr scripts | CCv2 discovers or builds the wrong thing. |
| 2. Build locally/CI | Execute the same application script that the selected deployment model requires | SSR-only bundle/server failure hidden by a CSR build. |
| 3. Inspect artifact | Locate dist/<app-name>/index.html, browser assets, and SSR output if enabled | Stale/misplaced artifact. |
| 4. Inspect served HTML | Confirm the non-empty resolved occ-backend-base-url meta value | Placeholder not replaced or hard-coded config overrides it. |
| 5. Test both callers | Browser CSR Network trace and SSR-to-OCC trace use intended host/site | Browser/Node endpoint divergence. |
| 6. Test transport policy | OPTIONS/GET probes, API filter decision, external domain/SmartEdit checks | CORS, 403, frame-policy, or header-size failures after promotion. |
| 7. Test deployment mode | Blue/green/canary preview uses the expected injected endpoint | Hard-coded endpoint bypasses deployment routing. |
| 8. Record evidence | Build ID, artifact checksum/version marker, URLs, configs (redacted), trace IDs, owner | Unrepeatable “it worked once” release claims. |
The expected outcome is boring: one reproducible artifact, one intentional runtime endpoint rule, one exact transport policy, and equal evidence for browser and SSR behavior. If any of those differ, the deployment is not ready to hide behind a successful build status.