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
Environment Variables
Create a .env file in your project root: 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.
Create Storefront Config
Create a shared config file that both the client and server storefronts will import: 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.
Create Server Storefront
Create a server-only module using the .server.ts convention. TanStack Start tree-shakes this out of the client bundle automatically. 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.
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 ;
}
Wire Into __root.tsx
Mount StorefrontBootstrap in the TanStack Router root so it runs on every page: 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
Context What to Use Import From Client-side public reads (catalog, categories) storefront.publicStorefront()lib/storefrontClient-side session flows (cart, wishlist, account) storefront.clientStorefront()lib/storefrontClient bootstrap storefront.bootstrap()lib/storefrontRoute loaders / pre-rendering storefront.publicStorefront()lib/storefrontServer functions (public reads) storefront.publicStorefront()lib/storefrontServer 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:
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:
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.
Use the head property in route definitions with loader data:
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.
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
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:
// 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
Level Issue Solution CRITICAL Importing storefront.server.ts in client code Server storefront must only be imported in server functions. Use the .server.ts file convention so TanStack Start tree-shakes it out. CRITICAL Using publicStorefront() for session-bound operations Use serverStorefront() from lib/storefront.server for auth, cart, and wishlist operations. HIGH Missing bootstrap in root layout Mount StorefrontBootstrap in __root.tsx. Without it, first-visit users will not have a session. HIGH Using clientStorefront() inside createServerFn Server functions run on the server. Use publicStorefront() for public reads or serverStorefront() for session-bound calls. MEDIUM Not excluding dynamic routes from pre-rendering Add a filter in the prerender config to skip search, user-specific, and API routes. MEDIUM Missing User-Agent on Cloudflare Workers Patch 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