> ## Documentation Index
> Fetch the complete documentation index at: https://docs.craveup.com/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" | "customer" | "receipt", resourceId: string) =>
  `storefront.${kind}.v1.${scope}.${storageKeyPart(resourceId)}`;
const cartKey = (locationId: string) => key("cart", locationId);
const customerKey = key("customer", "jwt");
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 customerTokenStore = {
  get: () => sessionStorage.getItem(customerKey),
  set: (token: string) => sessionStorage.setItem(customerKey, token),
  clear: () => sessionStorage.removeItem(customerKey),
};

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,
  getAuthToken: customerTokenStore.get,
});
```

The environment, merchant, and location scopes prevent a staging token or one merchant's token from being reused by another storefront. Persist the JWT returned by `customer.verifyOtp` with `customerTokenStore.set(token)`. 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; clear the customer JWT on logout and 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 paymentSession = await storefront.checkout.createPaymentSession(
  locationId,
  cart.id,
  { includeCustomerContext: true },
);
```

Omit `includeCustomerContext` for a guest checkout. A claimed cart already uses
the customer JWT because its guest capability has been cleared. Confirm the
Render the payment provider named by the returned session, then poll
`storefront.checkout.getOrderResult(locationId, cart.id)`. Handle all four
states: `payment_pending`, `order_pending`, `completed`, and `failed`. 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](/guides/deployment).

## Next steps

<CardGroup cols={2}>
  <Card title="Manage Cart" icon="cart-shopping" href="/guides/manage-cart">
    Add fulfillment, discounts, gratuity, and conflict handling.
  </Card>

  <Card title="Checkout Flow" icon="credit-card" href="/guides/checkout-flow">
    Confirm payment and poll the authoritative order result.
  </Card>

  <Card title="Storefront SDK" icon="code" href="/getting-started/storefront-sdk">
    Review the complete typed client surface.
  </Card>

  <Card title="Deployment" icon="cloud" href="/guides/deployment">
    Configure exact origins and production environment variables.
  </Card>
</CardGroup>
