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

# Next.js Quickstart

> Build a direct, capability-safe storefront with Next.js and the Crave Storefront SDK.

This quickstart connects a Next.js App Router project directly to the public Storefront API. No BFF or browser API key is required.

## Prerequisites

* Node.js 20+
* A published merchant slug and location ID
* The exact browser origin for local, preview, and production deployments registered with Crave
* A Stripe publishable key when you add checkout

## 1. Create the app and install the SDK

```bash theme={null}
npx create-next-app@latest my-storefront --typescript --app --tailwind
cd my-storefront
pnpm add --save-exact @craveup/storefront-sdk@2.0.1
```

## 2. Configure the public origin

<Snippet file="env-vars.mdx" />

The SDK requires an explicit `baseUrl`; it never derives an origin from a credential.

## 3. Create the browser client and scoped stores

```ts filename="src/lib/storefront-browser.ts" theme={null}
"use client";

import type {
  StorefrontCartSession,
  StorefrontSessionStore,
} from "@craveup/storefront-sdk";
import { createStorefrontClient } from "@craveup/storefront-sdk";

const apiUrl = process.env.NEXT_PUBLIC_CRAVEUP_API_URL;
const merchantSlug = process.env.NEXT_PUBLIC_CRAVEUP_MERCHANT_SLUG;
if (!apiUrl || !merchantSlug)
  throw new Error("Missing Storefront configuration");

const apiOrigin = new URL(apiUrl).origin;
const storageKeyPart = (value: string) =>
  encodeURIComponent(value).replaceAll(".", "%2E");
const environmentNamespace = storageKeyPart(apiOrigin);
const scope = `${environmentNamespace}.${storageKeyPart(merchantSlug)}`;
const key = (kind: "cart" | "receipt", resourceId: string) =>
  `storefront.${kind}.v1.${scope}.${storageKeyPart(resourceId)}`;
const cartKey = (locationId: string) => key("cart", locationId);
const receiptKey = (receiptId: string) => key("receipt", receiptId);

function readCartSession(locationId: string): StorefrontCartSession | null {
  const storageKey = cartKey(locationId);
  const raw = sessionStorage.getItem(storageKey);
  if (!raw) return null;

  try {
    const value = JSON.parse(raw) as Partial<StorefrontCartSession>;
    if (
      value.locationId !== locationId ||
      typeof value.cartId !== "string" ||
      typeof value.revision !== "number" ||
      !Number.isInteger(value.revision) ||
      value.revision < 0 ||
      (value.accessToken !== undefined && typeof value.accessToken !== "string")
    ) {
      throw new Error("Invalid cart session");
    }
    return value as StorefrontCartSession;
  } catch {
    sessionStorage.removeItem(storageKey);
    return null;
  }
}

export const sessionStore: StorefrontSessionStore = {
  get(locationId) {
    return readCartSession(locationId);
  },
  set(session) {
    sessionStorage.setItem(
      cartKey(session.locationId),
      JSON.stringify(session),
    );
  },
  clear(locationId) {
    sessionStorage.removeItem(cartKey(locationId));
  },
};

export const receiptTokenStore = {
  get: (receiptId: string) => sessionStorage.getItem(receiptKey(receiptId)),
  set: (receiptId: string, token: string) =>
    sessionStorage.setItem(receiptKey(receiptId), token),
  clear: (receiptId: string) =>
    sessionStorage.removeItem(receiptKey(receiptId)),
};

export const storefront = createStorefrontClient({
  baseUrl: apiOrigin,
  sessionStore,
  getMerchantSlug: () => merchantSlug,
});
```

The environment, merchant, and location scopes prevent one storefront's capabilities from being reused by another. For signed-in customer flows, create the browser client only after Clerk is ready and add `getAuthToken: () => getToken({ template: "crave-customer" })`; keep `getMerchantSlug` so the SDK can send explicit tenant routing. Clerk owns the session and token refresh—do not copy the JWT into application storage. Capture a receipt capability from the URL fragment, remove the fragment immediately, and keep it with `receiptTokenStore.set(receiptId, token)` only while the receipt is needed.

## 4. Create a separate server client

```ts filename="src/lib/storefront-server.ts" theme={null}
import { createStorefrontClient } from "@craveup/storefront-sdk";

export const storefrontServer = createStorefrontClient({
  baseUrl: process.env.NEXT_PUBLIC_CRAVEUP_API_URL!,
});
```

Use the server client only for anonymous published merchant, location, menu, product, distance, order-time, and gratuity calls. It deliberately has no `sessionStore` or `getAuthToken`. Never import the browser client into a Server Component, Route Handler, or Server Action.

## 5. Start a cart and mutate it

```tsx filename="src/app/order/page.tsx" theme={null}
"use client";

import { useEffect, useState } from "react";
import type { StorefrontCart } from "@craveup/storefront-sdk";
import { storefront } from "@/lib/storefront-browser";

const locationId = process.env.NEXT_PUBLIC_CRAVEUP_LOCATION_ID!;

export default function OrderPage() {
  const [cart, setCart] = useState<StorefrontCart | null>(null);

  useEffect(() => {
    void storefront.orderingSessions
      .start(locationId, { fulfillmentMethod: "takeout" })
      .then(({ cart }) => setCart(cart));
  }, []);

  async function addItem(productId: string) {
    if (!cart) return;
    setCart(
      await storefront.cart.addItem(locationId, cart.id, {
        productId,
        quantity: 1,
        selections: [],
        itemUnavailableAction: "remove_item",
      }),
    );
  }

  return (
    <main>
      <button onClick={() => addItem("prod_margherita")}>Add item</button>
      <p>{cart ? `${cart.totalQuantity} items` : "Starting cart…"}</p>
    </main>
  );
}
```

The SDK stores the returned cart capability, sends it only in a header, updates the saved revision from `ETag`, and generates idempotency keys. A `CART_CONFLICT` is surfaced to your UI; reload the cart before asking the shopper to retry. Clear the guest capability after claim, deletion, expiry, or terminal checkout handling, let Clerk clear its session on logout, and clear receipt capabilities after terminal display.

## 6. Complete checkout safely

For a signed-in shopper who still has a guest-capability cart, opt into the
customer context when discount validation or provider customer association needs
it:

```ts theme={null}
const outcome = await storefront.checkout.submitOrder(
  locationId,
  cart.id,
  {
    acceptedCatalogEvidence,
    acceptedDiscountEvidence,
    collectionMode: "ONLINE",
    customerConfirmationRevision,
    quoteFingerprint,
  },
  { idempotencyKey, includeCustomerContext: true, revision: cart.revision },
);
```

Omit `includeCustomerContext` for a guest checkout. A claimed cart already uses
the customer JWT because its guest capability has been cleared. If the outcome
is `CUSTOMER_ACTION_REQUIRED`, confirm its Stripe client secret using the exact
returned connected account, then poll
`storefront.checkout.getOrderSubmissionOutcome(locationId, cart.id, outcome.attemptId)`.
Treat only `PAID` as an online-payment success. Clear the cart session only after
terminal handling.

## 7. Register every browser origin

Register the exact origin for local development, each stable preview, and production before testing browser calls. An origin includes the scheme, hostname, and any non-default port. Do not request a wildcard and do not send a cart capability or customer JWT while working around a CORS failure. See [Deployment](/docs/guides/deployment.md).

## Next steps

> [card] [Manage Cart](/docs/guides/manage-cart.md)
>
> Add fulfillment, discounts, gratuity, and conflict handling.

> [card] [Checkout Flow](/docs/guides/checkout-flow.md)
>
> Confirm payment and poll the authoritative order result.

> [card] [Storefront SDK](/docs/getting-started/storefront-sdk.md)
>
> Review the complete typed client surface.

> [card] [Deployment](/docs/guides/deployment.md)
>
> Configure exact origins and production environment variables.


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