> ## 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.

# TanStack Start

> First-class TanStack Start wrapper with automatic token management, server functions, and pre-rendering support

**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

<Card title="@commercengine/storefront" icon="npm" href="https://www.npmjs.com/package/@commercengine/storefront">
  TanStack Start integration for Commerce Engine Storefront SDK. Import from `@commercengine/storefront/tanstack-start`.
</Card>

## Installation

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

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

  ```bash yarn theme={"theme":{"light":"github-light","dark":"github-dark"}}
  yarn add @commercengine/storefront
  ```
</CodeGroup>

## Quick Start

<Steps>
  <Step title="Environment Variables">
    Create a `.env` file in your project root:

    ```bash .env theme={"theme":{"light":"github-light","dark":"github-dark"}}
    VITE_STORE_ID=your-store-id
    VITE_API_KEY=your-api-key
    ```

    <Check>
      TanStack Start uses Vite, so client-exposed variables must use the `VITE_` prefix.
    </Check>
  </Step>

  <Step title="Create Storefront Config">
    Create a shared config file that both the client and server storefronts will import:

    ```typescript lib/storefront.ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
    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);
    ```

    <Info>
      `storefrontConfig` is exported separately so the server storefront can reuse the same configuration without duplicating values.
    </Info>
  </Step>

  <Step title="Create Server Storefront">
    Create a server-only module using the `.server.ts` convention. TanStack Start tree-shakes this out of the client bundle automatically.

    ```typescript lib/storefront.server.ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
    import { createTanStackStartServerStorefront } from "@commercengine/storefront/tanstack-start/server";
    import { storefrontConfig } from "./storefront";

    const factory = createTanStackStartServerStorefront(storefrontConfig);

    export function serverStorefront() {
      return factory.serverStorefront();
    }
    ```

    <Warning>
      The `@commercengine/storefront/tanstack-start/server` import includes a `server-only` guard. Never import `storefront.server.ts` from client code.
    </Warning>
  </Step>

  <Step title="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.

    ```tsx components/storefront-bootstrap.tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
    import { useEffect } from "react";
    import { storefront } from "@/lib/storefront";

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

  <Step title="Wire Into __root.tsx">
    Mount `StorefrontBootstrap` in the TanStack Router root so it runs on every page:

    ```tsx routes/__root.tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
    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>
      );
    }
    ```

    <Tip>
      `bootstrap()` is deduped internally — concurrent calls resolve to a single operation. You do not need to guard against multiple invocations.
    </Tip>
  </Step>
</Steps>

## Accessor Rules

| Context                                             | What to Use                     | Import From             |
| --------------------------------------------------- | ------------------------------- | ----------------------- |
| Client-side public reads (catalog, categories)      | `storefront.publicStorefront()` | `lib/storefront`        |
| Client-side session flows (cart, wishlist, account) | `storefront.clientStorefront()` | `lib/storefront`        |
| Client bootstrap                                    | `storefront.bootstrap()`        | `lib/storefront`        |
| Route loaders / pre-rendering                       | `storefront.publicStorefront()` | `lib/storefront`        |
| Server functions (public reads)                     | `storefront.publicStorefront()` | `lib/storefront`        |
| Server functions (session-bound)                    | `serverStorefront()`            | `lib/storefront.server` |

<Info>
  **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.
</Info>

## Key Patterns

### Route Loaders (Pre-rendering & SEO)

Use route loaders with `publicStorefront()` to pre-render catalog pages for SEO and fast initial loads:

```typescript routes/index.tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
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:

```typescript lib/hooks.ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
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"] }),
  });
}
```

<Tip>
  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.
</Tip>

### 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:

```typescript lib/server-fns/catalog.ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
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()`:

```typescript lib/server-fns/wishlist.ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
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;
  });
```

<Tip>
  Do not manually call `sdk.getUserId()` or pass `user_id` for wishlist and cart methods. The session-aware overloads resolve the user automatically.
</Tip>

### Head Metadata / SEO

Use the `head` property in route definitions with loader data:

```typescript routes/product/$slug.tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
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.

```typescript vite.config.ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
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"),
      },
    }),
  ],
});
```

<Info>
  Exclude dynamic routes (search, user-specific pages) from pre-rendering via the `filter` option. Only public, cacheable pages should be pre-rendered.
</Info>

## 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

```typescript lib/storefront.ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
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

```tsx components/storefront-bootstrap.tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
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;
}
```

<Warning>
  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.
</Warning>

## 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:

```typescript lib/storefront.ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
// 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 });
  };
}
```

<Note>
  This patch is only needed when deploying to Cloudflare Workers. Node.js and other runtimes send `User-Agent` automatically.
</Note>

## 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

<CardGroup cols={2}>
  <Card title="Shared Config" icon="gear">
    Export `storefrontConfig` from `lib/storefront.ts` and import it in `lib/storefront.server.ts`. Keep configuration in one place.
  </Card>

  <Card title="Server Boundaries" icon="shield-halved">
    Use the `.server.ts` file convention for session-bound code. TanStack Start enforces the boundary at build time.
  </Card>

  <Card title="Public vs Session" icon="key">
    Use `publicStorefront()` for catalog reads and `serverStorefront()` for user-scoped mutations. Never use `clientStorefront()` on the server.
  </Card>

  <Card title="React Query" icon="arrows-rotate">
    Wrap server functions with React Query hooks for client-side caching, refetching, and optimistic updates.
  </Card>

  <Card title="Pre-Rendering" icon="rocket">
    Enable `prerender` in `vite.config.ts` for static catalog pages. Exclude search and user-specific routes with the `filter` option.
  </Card>

  <Card title="Error Handling" icon="triangle-exclamation">
    Always check the `error` property in SDK responses. Throw inside server function handlers so React Query can surface errors to the UI.
  </Card>
</CardGroup>

## Cross-References

<CardGroup cols={2}>
  <Card title="SDK Client Overview" icon="book" href="/docs/sdk/client-overview">
    Core SDK client patterns and architecture
  </Card>

  <Card title="Token Management" icon="key" href="/docs/sdk/token-management">
    How cookie-backed sessions work across client and server
  </Card>

  <Card title="Hosted Checkout (React)" icon="cart-shopping" href="/docs/hosted-checkout/react">
    React integration for Commerce Engine Checkout
  </Card>

  <Card title="Authentication Guide" icon="user-shield" href="/docs/storefront/authentication">
    Complete authentication patterns for storefront implementations
  </Card>
</CardGroup>
