> ## Documentation Index
> Fetch the complete documentation index at: https://www.commercengine.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Node JS

> Complete guide to using the SDK in Node.js backend applications

Node.js integrations usually fall into two categories:

1. **Public backend reads** that do not carry an end-user session
2. **Session-bound requests** that must preserve one specific user's Commerce Engine tokens

Do not use one globally shared mutable session SDK for concurrent users.

## Installation

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @commercengine/storefront
```

## Shared public client

A public client has no user token state and can be shared by the process.

```typescript src/lib/public-storefront.ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { Environment, createStorefront } from "@commercengine/storefront";

export const storefront = createStorefront({
  storeId: process.env.CE_STORE_ID!,
  apiKey: process.env.CE_API_KEY!,
  environment:
    process.env.CE_ENV === "production"
      ? Environment.Production
      : Environment.Staging,
});

export const publicSdk = storefront.public();
```

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const { data, error } = await publicSdk.catalog.listProducts({
  page: 1,
  limit: 20,
});

if (error) throw new Error(error.message);
```

## Request-isolated session client

A backend route acting on behalf of a browser user should receive or retrieve that user's token and construct a session isolated to the request.

```typescript src/lib/session-storefront.ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import {
  Environment,
  MemoryTokenStorage,
  createStorefront,
} from "@commercengine/storefront";

export async function createRequestStorefront(accessToken: string, refreshToken?: string) {
  const tokenStorage = new MemoryTokenStorage();

  const storefront = createStorefront({
    storeId: process.env.CE_STORE_ID!,
    apiKey: process.env.CE_API_KEY!,
    environment:
      process.env.CE_ENV === "production"
        ? Environment.Production
        : Environment.Staging,
    session: {
      tokenStorage,
    },
  });

  const sdk = storefront.session();
  await sdk.setTokens(accessToken, refreshToken);
  return sdk;
}
```

<Warning>
  `MemoryTokenStorage` is process-local mutable state. Create it per request or per isolated job/user context. A singleton can leak one user's session into another request.
</Warning>

For durable BFF sessions, implement a `TokenStorage` bound to the authenticated application session—for example, a database or Redis record keyed by your server-side session ID. The storage instance must still be scoped to one user.

## Express route example

```typescript src/server.ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import express from "express";
import { publicSdk } from "./lib/public-storefront";
import { createRequestStorefront } from "./lib/session-storefront";

const app = express();

// Register the raw-body webhook route before any global body parser.
// The signature covers the exact bytes sent by Commerce Engine.
registerCommerceEngineWebhookRoute(app);

// Safe for the remaining application/json routes only after the webhook route.
app.use(express.json());

app.get("/api/products", async (req, res) => {
  const { data, error, response } = await publicSdk.catalog.listProducts({
    page: Number(req.query.page ?? 1),
    limit: 20,
  });

  if (error) {
    return res.status(response?.status ?? 502).json({ error: error.message });
  }

  return res.json(data);
});

app.get("/api/cart", async (req, res) => {
  const authorization = req.header("authorization");
  const accessToken = authorization?.startsWith("Bearer ")
    ? authorization.slice("Bearer ".length)
    : null;

  if (!accessToken) {
    return res.status(401).json({ error: "Bearer token required" });
  }

  const sdk = await createRequestStorefront(accessToken);
  const { data, error, response } = await sdk.cart.getUserCart();

  if (response?.status === 404) return res.json({ cart: null });
  if (error) {
    return res.status(response?.status ?? 502).json({ error: error.message });
  }

  return res.json(data);
});
```

Do not trust a user ID supplied by the browser when the SDK can resolve the current identity from the token.

## Backend-for-frontend token ownership

A BFF can keep Commerce Engine tokens out of browser JavaScript by storing them in a server-side session and exposing only your own HttpOnly session cookie.

In that architecture:

* the browser calls your BFF
* the BFF loads the user's CE token pair from server-side storage
* a request-bound Storefront session SDK performs the operation
* token updates are persisted back to the same server-side session
* Hosted Checkout cannot directly share those tokens unless you intentionally expose a secure synchronization mechanism

This is a different architecture from the browser-managed SDK wrappers. Choose one deliberately rather than combining parts of both.

## Background jobs

A background job should use credentials that belong to the job's intended context. Do not use an arbitrary storefront customer's token for system-level fulfilment work.

For order, payment, and shipment integrations, prefer Commerce Engine webhooks as the trigger. The storefront SDK can retrieve session-owned order data only when the job has a valid customer session. Administrative workflows may require a separate admin integration outside the Storefront SDK.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
async function processCustomerOrder(
  accessToken: string,
  orderNumber: string,
) {
  const sdk = await createRequestStorefront(accessToken);
  const { data, error } = await sdk.order.getOrderDetails({
    order_number: orderNumber,
  });

  if (error) throw new Error(error.message);
  return data;
}
```

## Webhook endpoint

Webhook verification must use the raw request body. Return success quickly and move heavy work to a queue.

```typescript src/server.ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import crypto from "node:crypto";
import express from "express";

function registerCommerceEngineWebhookRoute(app: express.Express) {
  app.post(
    "/webhooks/commerce-engine",
    // This parser must run before app.use(express.json()).
    express.raw({ type: "application/json" }),
    async (req, res) => {
      const signature = req.header("x-webhook-signature");
      const body = req.body;

      if (!Buffer.isBuffer(body)) {
        return res.status(400).json({ error: "Raw request body required" });
      }

      if (!signature || !verifySignature(body, signature)) {
        return res.status(401).json({ error: "Invalid signature" });
      }

      const event = JSON.parse(body.toString("utf8"));
      await queue.add("commerce-engine-webhook", event);
      return res.status(200).json({ received: true });
    },
  );
}

function verifySignature(body: Buffer, signature: string) {
  // Reject malformed input before decoding it as hexadecimal.
  if (!/^[a-f\d]{64}$/i.test(signature)) return false;

  const expected = crypto
    .createHmac("sha256", process.env.CE_WEBHOOK_SECRET!)
    .update(body)
    .digest();
  const received = Buffer.from(signature, "hex");

  return (
    received.length === expected.length &&
    crypto.timingSafeEqual(received, expected)
  );
}
```

<Warning>
  `express.raw()` cannot recover the original bytes after `express.json()` has parsed the request. Register this webhook route before the global JSON middleware, as shown in the Express setup above. An alternative is to explicitly exclude this path from the global parser, but do not run both parsers for the webhook request.
</Warning>

Make processing idempotent and tolerate duplicate or out-of-order deliveries. Retrieve the exact current event payload from the LLM-first webhook reference before implementing a handler.

## Health checks

A health endpoint should not create an end-user session. Use a public operation and distinguish service health from catalog emptiness.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
app.get("/health", async (_req, res) => {
  const { error, response } = await publicSdk.store.getStoreConfig();

  if (error) {
    return res.status(503).json({
      status: "unhealthy",
      upstreamStatus: response?.status,
    });
  }

  return res.json({ status: "healthy" });
});
```

Do not include API keys, tokens, full upstream responses, or customer data in health output.

## Timeouts and cancellation

Set request timeouts according to the application's latency budget and propagate client cancellation where the SDK operation supports the underlying fetch signal. Do not blindly retry validation, authorization, cart mutation, or order-creation requests.

For idempotent public reads, a bounded retry at the application layer may be appropriate. For mutations, understand the operation's idempotency semantics before retrying.

## Logging and observability

Log:

* operation name
* duration
* HTTP status
* Commerce Engine request ID when available
* application request ID
* error code/message after redaction

Never log:

* access or refresh tokens
* API credentials beyond non-sensitive store identifiers
* card or payment authentication data
* full customer addresses or contact details

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const started = performance.now();
const { data, error, response } = await publicSdk.catalog.listCategories();

logger.info({
  operation: "catalog.listCategories",
  durationMs: performance.now() - started,
  status: response?.status,
  success: !error,
});
```

## Deployment runtime

Node.js APIs are not automatically portable to every edge runtime. Check the selected framework/platform:

* Vercel Node functions support Node APIs
* Cloudflare Workers use the workerd runtime and may require framework-specific adapters or API replacements
* Netlify supports Node and edge execution with different capability sets

For storefront frameworks, prefer the first-party framework wrapper and the starter's deployment adapter over recreating an SSR binding in a generic Express layer.

## Production checklist

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
[ ] public SDK shared only for non-session reads
[ ] session SDK/storage isolated per user or request
[ ] browser-supplied user IDs are not trusted unnecessarily
[ ] token updates persist to the correct server-side session
[ ] webhook signature uses the raw body
[ ] webhook processing is idempotent and asynchronous
[ ] mutations are not blindly retried
[ ] secrets and customer data are redacted from logs
[ ] runtime capabilities match the deployment target
[ ] typecheck, tests, lint, and production build pass
```

<Card title="Webhook skill and reference" icon="webhook" href="https://github.com/commercengine/skills">
  Install the Commerce Engine skills package and retrieve exact webhook payload contracts from the LLM-first reference.
</Card>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.