Skip to main content
The @commercengine/storefront/nextjs package provides a Next.js-specific wrapper around the core Storefront SDK. It gives you three accessors — publicStorefront(), serverStorefront(), and clientStorefront() — so the right token strategy is used automatically in every rendering context.

NPM Package

@commercengine/storefront

Next.js integration for Commerce Engine Storefront SDK. Import from @commercengine/storefront/nextjs.
@commercengine/storefront-sdk-nextjs is deprecated. See the migration table below if you are upgrading.

Installation

npm install @commercengine/storefront

Quick Start

1

Set Environment Variables

Add your store credentials to .env.local:
.env.local
NEXT_PUBLIC_STORE_ID=your-store-id
NEXT_PUBLIC_API_KEY=your-api-key
NEXT_PUBLIC_API_KEY is safe for client-side use — it is scoped to public storefront operations.
2

Create the Storefront Config

lib/storefront.ts
import { Environment } from "@commercengine/storefront";
import { createNextjsStorefront } from "@commercengine/storefront/nextjs";

export const storefront = createNextjsStorefront({
  storeId: process.env.NEXT_PUBLIC_STORE_ID!,
  apiKey: process.env.NEXT_PUBLIC_API_KEY!,
  environment: Environment.Staging, // or Environment.Production
  tokenStorageOptions: { prefix: "myapp_" },
});
createNextjsStorefront() does not infer credentials from environment variables — you must pass storeId and apiKey explicitly.
3

Create the StorefrontBootstrap Component

A Client Component that establishes the anonymous session on first visit. It renders nothing visible.
components/storefront-bootstrap.tsx
"use client";

import { useEffect } from "react";
import { storefront } from "@/lib/storefront";

export function StorefrontBootstrap() {
  useEffect(() => {
    storefront.bootstrap().catch(console.error);
  }, []);
  return null;
}
bootstrap() is deduped and idempotent. If session cookies already exist (returning user), it is a no-op.
4

Mount in Root Layout

The root layout stays a Server Component — StorefrontBootstrap is a Client Component child rendered inside it.
app/layout.tsx
import { storefront } from "@/lib/storefront";
import { StorefrontBootstrap } from "@/components/storefront-bootstrap";

const { data: storeConfig } = await storefront
  .publicStorefront()
  .store.getStoreConfig();

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <StorefrontBootstrap />
        <header>{storeConfig?.store_config?.brand.name}</header>
        {children}
      </body>
    </html>
  );
}
Always use publicStorefront() in the root layout. Do not call serverStorefront() or clientStorefront() here — the root layout should not participate in the user session.

Accessor Rules

Use the right accessor for each rendering context:
ContextAccessorWhy
Root Layoutstorefront.publicStorefront()Root layouts should stay public — no live user session
Build time / SSG / generateMetadatastorefront.publicStorefront()No request context at build time
Public Server Componentstorefront.publicStorefront()Public reads that don’t need a session
Session-aware Server Componentawait storefront.serverStorefront()Auto-reads cookies via next/headers
Server Action / Route Handlerawait storefront.serverStorefront()Can read and write cookies
Client Componentstorefront.clientStorefront()Uses the browser-side session client
serverStorefront() is async — it dynamically imports next/headers and uses React cache() to dedupe within a single request. clientStorefront() throws if called on the server. serverStorefront() throws if called in the browser.

Key Patterns

Public Server Component

Fetch catalog data without touching the user session.
app/products/page.tsx
import { storefront } from "@/lib/storefront";

export default async function ProductsPage() {
  const sdk = storefront.publicStorefront();
  const { data, error } = await sdk.catalog.listProducts({ page: 1, limit: 20 });

  if (error) return <p>Error: {error.message}</p>;

  return (
    <div>
      {data?.products.map((product) => (
        <div key={product.id}>{product.name}</div>
      ))}
    </div>
  );
}

Session-Aware Server Component

Read session-scoped data such as account details or wish lists.
app/account/page.tsx
import { storefront } from "@/lib/storefront";

export default async function AccountPage() {
  const sdk = await storefront.serverStorefront();
  const { data, error } = await sdk.customer.getCustomer();

  if (error) return <p>Error: {error.message}</p>;
  return <div>Welcome, {data?.customer?.first_name}</div>;
}

Server Actions (Mutations)

Server Actions can read and write cookies, making them ideal for auth and cart mutations.
app/actions.ts
"use server";

import { storefront } from "@/lib/storefront";

export async function loginWithEmail(email: string) {
  const sdk = await storefront.serverStorefront();

  const { data, error } = await sdk.auth.loginWithEmail({
    email,
    register_if_not_exists: true,
  });

  if (error) return { error: error.message };
  return { otp_token: data?.otp_token, otp_action: data?.otp_action };
}

export async function addToCartAction(
  cartId: string,
  productId: string,
  variantId: string | null,
) {
  const sdk = await storefront.serverStorefront();
  const { data, error } = await sdk.cart.addDeleteCartItem(
    { id: cartId },
    { product_id: productId, variant_id: variantId, quantity: 1 }
  );
  return { data: data?.cart, error: error?.message };
}

Static Site Generation (SSG)

Use publicStorefront() for generateStaticParams and any build-time rendering — no session is available at build time.
app/products/[slug]/page.tsx
import { storefront } from "@/lib/storefront";

export async function generateStaticParams() {
  const sdk = storefront.publicStorefront();
  const { data } = await sdk.catalog.listProducts({ limit: 100 });

  return (data?.products ?? []).map((product) => ({
    slug: product.slug || product.id,
  }));
}

export default async function ProductPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const sdk = storefront.publicStorefront();
  const { data, error } = await sdk.catalog.getProductDetail({
    product_id_or_slug: slug,
  });

  if (error || !data) return <p>Product not found</p>;
  return <h1>{data.product.name}</h1>;
}

Client Component

Use clientStorefront() for browser-side interactions like add-to-cart buttons.
components/add-to-cart-button.tsx
"use client";

import { storefront } from "@/lib/storefront";

export function AddToCartButton({
  cartId,
  productId,
  variantId,
}: {
  cartId: string;
  productId: string;
  variantId: string | null;
}) {
  async function handleClick() {
    const sdk = storefront.clientStorefront();
    await sdk.cart.addDeleteCartItem(
      { id: cartId },
      { product_id: productId, variant_id: variantId, quantity: 1 }
    );
  }

  return <button onClick={handleClick}>Add to Cart</button>;
}

SEO Metadata

generateMetadata runs at build time or request time without a live session — use publicStorefront().
app/products/[slug]/page.tsx
import type { Metadata } from "next";
import { storefront } from "@/lib/storefront";

export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>;
}): Promise<Metadata> {
  const { slug } = await params;
  const sdk = storefront.publicStorefront();
  const { data } = await sdk.catalog.getProductDetail({
    product_id_or_slug: slug,
  });

  const product = data?.product;
  if (!product) return { title: "Product Not Found" };

  return {
    title: product.name,
    description: product.short_description ?? undefined,
  };
}

Session Helpers

The storefront config accepts tokenStorageOptions to control cookie behavior:
PropertyDefaultNotes
prefix"ce_"Cookie name prefix
maxAge2592000 (30 days)Cookie max-age in seconds
path"/"Available across all routes
domain---Defaults to request domain
secureauto-detectedtrue in production (HTTPS)
sameSite"lax"CSRF protection
Client and server cookie configurations are automatically aligned — you do not need to configure them separately. For the full token lifecycle and returning-user flow, see Token Management.

Hosted Checkout + Next.js

If you use the Commerce Engine Hosted Checkout, follow this three-step pattern to keep the storefront SDK session and checkout session in sync. No provider wrapper is needed.
1

Add onTokensUpdated to Storefront Config

This callback forwards token updates from the storefront SDK to the checkout iframe.
lib/storefront.ts
import { Environment } from "@commercengine/storefront";
import { createNextjsStorefront } from "@commercengine/storefront/nextjs";

export const storefront = createNextjsStorefront({
  storeId: process.env.NEXT_PUBLIC_STORE_ID!,
  apiKey: process.env.NEXT_PUBLIC_API_KEY!,
  environment: Environment.Staging,
  tokenStorageOptions: { prefix: "myapp_" },
  onTokensUpdated: (accessToken, refreshToken) => {
    if (typeof window !== "undefined") {
      void import("@commercengine/checkout").then(({ getCheckout }) => {
        getCheckout().updateTokens(accessToken, refreshToken);
      });
    }
  },
});
2

Bootstrap + Init Checkout in a Root Client Component

Call both storefront.bootstrap() and initCheckout() in a single Client Component. The checkout is initialized with authMode: "provided" so the storefront SDK owns the session.
components/storefront-bootstrap.tsx
"use client";

import { useEffect } from "react";
import { initCheckout, destroyCheckout } from "@commercengine/checkout/react";
import { storefront } from "@/lib/storefront";

export function StorefrontBootstrap() {
  useEffect(() => {
    async function init() {
      await storefront.bootstrap();

      const sdk = storefront.clientStorefront();
      const accessToken = await sdk.getAccessToken();
      const refreshToken = await sdk.session.peekRefreshToken();

      initCheckout({
        storeId: process.env.NEXT_PUBLIC_STORE_ID!,
        apiKey: process.env.NEXT_PUBLIC_API_KEY!,
        authMode: "provided",
        accessToken: accessToken ?? undefined,
        refreshToken: refreshToken ?? undefined,
        onTokensUpdated: ({ accessToken, refreshToken }) => {
          void sdk.setTokens(accessToken, refreshToken);
        },
      });
    }

    init();
    return () => destroyCheckout();
  }, []);

  return null;
}
3

Mount in Root Layout

app/layout.tsx
import { StorefrontBootstrap } from "@/components/storefront-bootstrap";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <StorefrontBootstrap />
        {children}
      </body>
    </html>
  );
}
Then use useCheckout() in any Client Component — no provider needed:
components/cart-button.tsx
"use client";
import { useCheckout } from "@commercengine/checkout/react";

function CartButton() {
  const { cartCount, openCart, isReady } = useCheckout();
  return (
    <button onClick={openCart} disabled={!isReady}>Cart ({cartCount})</button>
  );
}

Migration from Deprecated Package

If you are migrating from @commercengine/storefront-sdk-nextjs, use this mapping:
Old (storefront-sdk-nextjs)New (storefront/nextjs)
@commercengine/storefront-sdk-nextjs@commercengine/storefront/nextjs
createStorefront()createNextjsStorefront({ storeId, apiKey, ... })
storefront.public()storefront.publicStorefront()
storefront.session() (client)storefront.clientStorefront()
storefront.session(await cookies())await storefront.serverStorefront()
StorefrontSDKInitializerCustom StorefrontBootstrap component calling storefront.bootstrap()
Env var auto-inferenceExplicit storeId and apiKey in config
storefront({ isRootLayout: true })storefront.publicStorefront()
NEXT_BUILD_CACHE_TOKENS env flagNot needed — use publicStorefront() for build-time reads

Common Pitfalls

LevelIssueSolution
CRITICALUsing publicStorefront() for cart, auth, or order flowsUse await serverStorefront() on the server or clientStorefront() on the client
CRITICALUsing clientStorefront() on the serverclientStorefront() throws on the server — use await storefront.serverStorefront()
HIGHMissing StorefrontBootstrap in root layoutMount a Client Component calling storefront.bootstrap() in the root layout
HIGHCalling serverStorefront() in a Client ComponentClient Components must use storefront.clientStorefront()
HIGHUsing session accessors in SSG or root layoutUse storefront.publicStorefront() for build-time and root-layout reads
MEDIUMSession-aware data in the root layoutMove it into a nested Server Component and use await storefront.serverStorefront()

Best Practices

One Config File

Create a single lib/storefront.ts that calls createNextjsStorefront() and export storefront from it. Import this everywhere.

Right Accessor, Right Context

publicStorefront() for public reads, serverStorefront() for session-aware server code, clientStorefront() for browser interactions.

Server Actions for Mutations

Auth, cart updates, and order creation should go through Server Actions using await serverStorefront() so cookies are read and written correctly.

Error Handling

Every SDK call returns { data, error }. Always check error before using data and implement error boundaries for graceful failures.

Cross-References

SDK Installation

Core SDK setup and framework detection guide.

Token Management

Full token lifecycle, cookie hydration, and returning-user flow.

Authentication Guide

OTP, email, and social login patterns for storefronts.

Hosted Checkout

Full hosted checkout integration with the React hook API.