Skip to main content
TanStack Start wrapper for the CommerceEngine Storefront SDK. Provides cookie-backed session management, server function support, and pre-rendering — all wired automatically through @commercengine/storefront/tanstack-start.

NPM Package

@commercengine/storefront

TanStack Start integration for Commerce Engine Storefront SDK. Import from @commercengine/storefront/tanstack-start.

Installation

npm install @commercengine/storefront

Quick Start

1

Environment Variables

Create a .env file in your project root:
.env
VITE_STORE_ID=your-store-id
VITE_API_KEY=your-api-key
TanStack Start uses Vite, so client-exposed variables must use the VITE_ prefix.
2

Create Storefront Config

Create a shared config file that both the client and server storefronts will import:
lib/storefront.ts
import { Environment } from "@commercengine/storefront";
import { createTanStackStartStorefront } from "@commercengine/storefront/tanstack-start";

export const storefrontConfig = {
  storeId: import.meta.env.VITE_STORE_ID,
  apiKey: import.meta.env.VITE_API_KEY,
  environment: Environment.Staging,
  tokenStorageOptions: { prefix: "myapp_" },
};

export const storefront = createTanStackStartStorefront(storefrontConfig);
storefrontConfig is exported separately so the server storefront can reuse the same configuration without duplicating values.
3

Create Server Storefront

Create a server-only module using the .server.ts convention. TanStack Start tree-shakes this out of the client bundle automatically.
lib/storefront.server.ts
import { createTanStackStartServerStorefront } from "@commercengine/storefront/tanstack-start/server";
import { storefrontConfig } from "./storefront";

const factory = createTanStackStartServerStorefront(storefrontConfig);

export function serverStorefront() {
  return factory.serverStorefront();
}
The @commercengine/storefront/tanstack-start/server import includes a server-only guard. Never import storefront.server.ts from client code.
4

Create StorefrontBootstrap Component

The bootstrap component establishes the client session on first visit. It is a no-op when cookies already exist from a returning user.
components/storefront-bootstrap.tsx
import { useEffect } from "react";
import { storefront } from "@/lib/storefront";

export function StorefrontBootstrap() {
  useEffect(() => {
    storefront.bootstrap().catch(console.error);
  }, []);
  return null;
}
5

Wire Into __root.tsx

Mount StorefrontBootstrap in the TanStack Router root so it runs on every page:
routes/__root.tsx
import { createRootRoute, Outlet } from "@tanstack/react-router";
import { StorefrontBootstrap } from "@/components/storefront-bootstrap";

export const Route = createRootRoute({
  component: RootComponent,
});

function RootComponent() {
  return (
    <html lang="en">
      <body>
        <StorefrontBootstrap />
        <Outlet />
      </body>
    </html>
  );
}
bootstrap() is deduped internally — concurrent calls resolve to a single operation. You do not need to guard against multiple invocations.

Accessor Rules

ContextWhat to UseImport From
Client-side public reads (catalog, categories)storefront.publicStorefront()lib/storefront
Client-side session flows (cart, wishlist, account)storefront.clientStorefront()lib/storefront
Client bootstrapstorefront.bootstrap()lib/storefront
Route loaders / pre-renderingstorefront.publicStorefront()lib/storefront
Server functions (public reads)storefront.publicStorefront()lib/storefront
Server functions (session-bound)serverStorefront()lib/storefront.server
Pre-render catalog pages with route loaders for SEO and fast initial loads. Once the app is hydrated, client-side fetching is equally fast and much less complex — use it for all subsequent navigation and interactions. Server functions are optional; the SDK talks to a public API with no secrets to protect.

Key Patterns

Route Loaders (Pre-rendering & SEO)

Use route loaders with publicStorefront() to pre-render catalog pages for SEO and fast initial loads:
routes/index.tsx
import { createFileRoute } from "@tanstack/react-router";
import { storefront } from "@/lib/storefront";

export const Route = createFileRoute("/")({
  loader: async () => {
    const sdk = storefront.publicStorefront();
    const { data, error } = await sdk.catalog.listSkus({ page: 1, limit: 20 });
    if (error) console.error("Failed to load products:", error.message);
    return { products: data?.skus ?? [] };
  },
  component: HomePage,
});

function HomePage() {
  const { products } = Route.useLoaderData();
  return (
    <div>
      {products.map((product) => (
        <div key={product.sku}>{product.variant_name || product.product_name}</div>
      ))}
    </div>
  );
}

Client-Side Fetching (After Hydration)

Once the app is hydrated, client-side fetching is equally fast and much less complex. Use React Query with the SDK directly — no server functions needed:
lib/hooks.ts
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { storefront } from "@/lib/storefront";

export function useProducts(options: { page?: number; limit?: number } = {}) {
  return useQuery({
    queryKey: ["products", options],
    queryFn: async () => {
      const sdk = storefront.publicStorefront();
      const { data, error } = await sdk.catalog.listProducts({
        page: options.page ?? 1,
        limit: options.limit ?? 20,
      });
      if (error) throw new Error(error.message);
      return data;
    },
  });
}

export function useWishlist() {
  return useQuery({
    queryKey: ["wishlist"],
    queryFn: async () => {
      const sdk = storefront.clientStorefront();
      const { data, error } = await sdk.cart.getWishlist();
      if (error) throw new Error(error.message);
      return data ?? { products: [] };
    },
  });
}

export function useAddToWishlist() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: async ({ productId, variantId }: { productId: string; variantId?: string | null }) => {
      const sdk = storefront.clientStorefront();
      const { data, error } = await sdk.cart.addToWishlist({
        product_id: productId,
        variant_id: variantId ?? null,
      });
      if (error) throw new Error(error.message);
      return data;
    },
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ["wishlist"] }),
  });
}
Use publicStorefront() for catalog reads (no session needed). Use clientStorefront() for session-bound operations like wishlist, cart, and account. The session-aware overloads resolve user_id automatically.

Server Functions (Optional)

Server functions are not required for most storefront reads. Use them if you want to run logic at the edge or need specific server-side behavior. Keep them thin:
lib/server-fns/catalog.ts
import { createServerFn } from "@tanstack/react-start";
import { storefront } from "@/lib/storefront";

export const fetchProducts = createServerFn({ method: "GET" })
  .inputValidator((d: { page?: number; limit?: number } | undefined) => d ?? {})
  .handler(async ({ data }) => {
    const sdk = storefront.publicStorefront();
    const { data: result, error } = await sdk.catalog.listProducts({
      page: data.page ?? 1,
      limit: data.limit ?? 20,
    });
    if (error) throw new Error(error.message);
    return result;
  });
For session-bound server functions (e.g., server-side wishlist mutations), use serverStorefront():
lib/server-fns/wishlist.ts
import { createServerFn } from "@tanstack/react-start";
import { serverStorefront } from "@/lib/storefront.server";

export const addToWishlist = createServerFn({ method: "POST" })
  .inputValidator((d: { productId: string; variantId?: string | null }) => d)
  .handler(async ({ data }) => {
    const sdk = serverStorefront();
    const { data: result, error } = await sdk.cart.addToWishlist(
      { product_id: data.productId, variant_id: data.variantId ?? null }
    );
    if (error) throw new Error(error.message);
    return result;
  });
Do not manually call sdk.getUserId() or pass user_id for wishlist and cart methods. The session-aware overloads resolve the user automatically.

Head Metadata / SEO

Use the head property in route definitions with loader data:
routes/product/$slug.tsx
import { createFileRoute } from "@tanstack/react-router";
import { storefront } from "@/lib/storefront";

export const Route = createFileRoute("/product/$slug")({
  loader: async ({ params }) => {
    const sdk = storefront.publicStorefront();
    const { data } = await sdk.catalog.getProductDetail({
      product_id: params.slug,
    });
    return { product: data?.product ?? null };
  },
  head: ({ loaderData }) => ({
    meta: [
      { title: loaderData?.product?.name ?? "Product Not Found" },
      {
        name: "description",
        content: loaderData?.product?.short_description ?? "",
      },
    ],
  }),
  component: ProductPage,
});

function ProductPage() {
  const { product } = Route.useLoaderData();
  if (!product) return <div>Product not found</div>;
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.short_description}</p>
    </div>
  );
}

Pre-Rendering

Enable pre-rendering in your Vite config. Pre-rendered routes automatically use publicStorefront() from route loaders.
vite.config.ts
import { defineConfig } from "vite";
import { tanstackStart } from "@tanstack/react-start/plugin";

export default defineConfig({
  plugins: [
    tanstackStart({
      prerender: {
        enabled: true,
        crawlLinks: true,
        autoStaticPathsDiscovery: true,
        filter: ({ path }) =>
          !path.startsWith("/search") && !path.endsWith(".xml"),
      },
    }),
  ],
});
Exclude dynamic routes (search, user-specific pages) from pre-rendering via the filter option. Only public, cacheable pages should be pre-rendered.

Hosted Checkout Integration

If you use Commerce Engine’s hosted checkout, wire token synchronization between the storefront SDK and the checkout iframe.

Step 1: Add onTokensUpdated to Config

lib/storefront.ts
import { Environment } from "@commercengine/storefront";
import { createTanStackStartStorefront } from "@commercengine/storefront/tanstack-start";

export const storefrontConfig = {
  storeId: import.meta.env.VITE_STORE_ID,
  apiKey: import.meta.env.VITE_API_KEY,
  environment: Environment.Staging,
  tokenStorageOptions: { prefix: "myapp_" },
  onTokensUpdated: (accessToken: string, refreshToken: string) => {
    if (typeof window !== "undefined") {
      void import("@commercengine/checkout").then(({ getCheckout }) => {
        getCheckout().updateTokens(accessToken, refreshToken);
      });
    }
  },
};

export const storefront = createTanStackStartStorefront(storefrontConfig);

Step 2: Initialize Checkout in Bootstrap

components/storefront-bootstrap.tsx
import { useEffect } from "react";
import { initCheckout } from "@commercengine/checkout";
import { destroyCheckout } from "@commercengine/checkout/react";
import { storefront } from "@/lib/storefront";

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

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

      initCheckout({
        storeId: import.meta.env.VITE_STORE_ID,
        apiKey: import.meta.env.VITE_API_KEY,
        authMode: "provided",
        accessToken: accessToken ?? undefined,
        refreshToken: refreshToken ?? undefined,
        onTokensUpdated: ({ accessToken, refreshToken }) => {
          void sdk.setTokens(accessToken, refreshToken);
        },
      });
    };
    void init();
    return () => destroyCheckout();
  }, []);
  return null;
}
Install @commercengine/checkout separately if you use hosted checkout. The authMode: "provided" setting tells checkout to use your storefront SDK tokens instead of managing its own.

Cloudflare Workers

Cloudflare Workers (workerd runtime) does not send a User-Agent header by default. The Commerce Engine API requires one. Patch global fetch at the top of your storefront config:
lib/storefront.ts
// Add at the top of the file, before config
if (typeof window === "undefined") {
  const originalFetch = globalThis.fetch;
  globalThis.fetch = (input: RequestInfo | URL, init?: RequestInit) => {
    const headers = new Headers(
      input instanceof Request ? input.headers : init?.headers
    );
    if (!headers.has("User-Agent")) {
      headers.set("User-Agent", "myapp/1.0");
    }
    if (input instanceof Request) {
      return originalFetch(new Request(input, { headers }), init);
    }
    return originalFetch(input, { ...init, headers });
  };
}
This patch is only needed when deploying to Cloudflare Workers. Node.js and other runtimes send User-Agent automatically.

Common Pitfalls

LevelIssueSolution
CRITICALImporting storefront.server.ts in client codeServer storefront must only be imported in server functions. Use the .server.ts file convention so TanStack Start tree-shakes it out.
CRITICALUsing publicStorefront() for session-bound operationsUse serverStorefront() from lib/storefront.server for auth, cart, and wishlist operations.
HIGHMissing bootstrap in root layoutMount StorefrontBootstrap in __root.tsx. Without it, first-visit users will not have a session.
HIGHUsing clientStorefront() inside createServerFnServer functions run on the server. Use publicStorefront() for public reads or serverStorefront() for session-bound calls.
MEDIUMNot excluding dynamic routes from pre-renderingAdd a filter in the prerender config to skip search, user-specific, and API routes.
MEDIUMMissing User-Agent on Cloudflare WorkersPatch global fetch to inject a User-Agent header (see above).

Best Practices

Shared Config

Export storefrontConfig from lib/storefront.ts and import it in lib/storefront.server.ts. Keep configuration in one place.

Server Boundaries

Use the .server.ts file convention for session-bound code. TanStack Start enforces the boundary at build time.

Public vs Session

Use publicStorefront() for catalog reads and serverStorefront() for user-scoped mutations. Never use clientStorefront() on the server.

React Query

Wrap server functions with React Query hooks for client-side caching, refetching, and optimistic updates.

Pre-Rendering

Enable prerender in vite.config.ts for static catalog pages. Exclude search and user-specific routes with the filter option.

Error Handling

Always check the error property in SDK responses. Throw inside server function handlers so React Query can surface errors to the UI.

Cross-References