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

# Storage and server rendering

> How the kit remembers the cart, and how a server and a browser agree on the same one.

## What the kit remembers

Four opaque handles and one preference, never prices, personal data or credentials:

| Key | What | Lifetime |
| - | - | - |
| `<prefix>_cart_id` | The shopper's cart | `cartTtlSeconds`, default 30 days |
| `<prefix>_store_id` | The shop's numeric id, noted off every cart | Same as the cart; kept when the cart is forgotten |
| `<prefix>_checkout_session` | The current handoff handle (a session id, or a legacy order uuid) | 24 hours |
| `<prefix>_checkout_store` | The numeric shop id again, kept beside the session | With the checkout session |
| `<prefix>_currency` | The currency the shopper picked with `setCurrency()` (`"EUR"`), only once they pick one | A year |

The default prefix is `qb` (`qb_cart_id`, …). The first four are server-side handles, so a leaked or stale value costs at most a fresh cart, and "just clear it" is always a valid recovery. The currency is a display preference, validated on every read so a tampered value is ignored rather than sent. Because it is a cookie by default, a server render with request-cookie storage prices the page in the currency the browser picked. See [Currencies](/kit/concepts/currencies).

The shopper's cookie consent decision is a separate cookie, `qb_consent`, owned by the consent layer rather than the storage adapter. See [Consent and analytics](/kit/concepts/consent-and-analytics#the-consent-store).

The shop id is stored twice because the copies expire on different clocks: the platform drops the cart the moment an order exists, but the thank-you page and the [embedded checkout's return leg](/kit/concepts/embedded-checkout#redirect-payment-methods) still need the id. `checkout.finalize()` forgets the cart and the session; only the cart TTL forgets the shop id.

## Picking an adapter

`storage` accepts `"auto"` (default), `"cookie"`, `"localStorage"`, `"memory"` or your own adapter. `"auto"` resolves like this:

<Steps>
  <Step title="A cookies accessor was supplied">Server cookie storage. Supplying one always wins.</Step>
  <Step title="Not a browser">Memory.</Step>
  <Step title="document.cookie is usable">Document cookies.</Step>
  <Step title="localStorage is usable">`localStorage`.</Step>
  <Step title="Otherwise">Memory.</Step>
</Steps>

**Cookies beat `localStorage` on purpose.** A cookie is the only client-side store the server can also read, which is what lets a server render see the same cart as the browser.

Resolution never throws. An unusable preference degrades to memory, because a new cart is better than a blank server render. Check `shopkit.runtime.persistent` (`false` means memory) when a cart does not survive a reload.

## Server-side cookies

Supply a `CookieAccessor`. Every method may return a promise.

| Method | Required | Used for |
| - | - | - |
| `get(name)` | Yes | Reading the cart, checkout session, currency and consent cookies |
| `set(name, value, attributes)` | No | Remembering a cart or session created on the server. Without it, writes are silently dropped |
| `remove(name, attributes)` | No | Forgetting them |
| `getAll()` | No | Letting `checkout.start()` find the Google Analytics `_ga_<property>` cookie (a name the kit cannot know in advance) and forward the GA session to the hosted checkout. Without it the consent decision is still forwarded, the GA ids are not |

### Next.js App Router

A server component cannot set cookies and a server action can, so use two clients:

```ts lib/shopkit.ts theme={null}
import { cookies } from "next/headers"
import { createShopkitClient } from "@quickbutik/kit"

const baseConfig = () => ({
  publishableKey: process.env.NEXT_PUBLIC_QUICKBUTIK_PUBLISHABLE_KEY!,
})

// Server components: read-only. Writes are dropped.
export async function readOnlyShopkit() {
  const jar = await cookies()
  return createShopkitClient({
    ...baseConfig(),
    cookies: { get: (name) => jar.get(name)?.value ?? null },
  })
}

// Server actions and route handlers: writable.
export async function writableShopkit() {
  const jar = await cookies()
  return createShopkitClient({
    ...baseConfig(),
    cookies: {
      get: (name) => jar.get(name)?.value ?? null,
      getAll: () => jar.getAll(),   // the GA ids for the checkout handoff
      set: (name, value, attrs) => {
        jar.set(name, value, {
          path: attrs.path ?? "/",
          maxAge: attrs.maxAge,
          sameSite: attrs.sameSite ?? "lax",
          secure: attrs.secure ?? false,
          httpOnly: false, // the browser half reads the same cookies
        })
      },
      remove: (name) => { jar.delete(name) },
    },
  })
}
```

<Warning>
  In a server component call `cart.current()`, which never creates a cart. Keep `ensure()`, `add()` and `checkout.start()` in a server action, a route handler or the browser, where the cookie can actually be written.
</Warning>

### Framework-free: `createRequestCookieStorage`

Reads cookies off a `Request`, a `Headers` object or a raw cookie header string, and optionally collects writes as `Set-Cookie` strings. The fit for Astro endpoints, TanStack Start, Remix loaders, Hono, Express and Workers:

```ts theme={null}
import { createRequestCookieStorage, createShopkitClient } from "@quickbutik/kit"

const setCookies: string[] = []
const shopkit = createShopkitClient({ publishableKey }).withStorage(
  createRequestCookieStorage(request, {
    collect: setCookies,
    attributes: { secure: true, sameSite: "lax" },
  }),
)

// …after handling the request:
for (const cookie of setCookies) response.headers.append("set-cookie", cookie)
```

In Express, pass `req.headers.cookie` as the source and `res.append("Set-Cookie", cookie)` for each collected string. Pass `secure: true` explicitly on a server: there is no ambient `location` to infer https from.

### `withStorage`: one config, many requests

`withStorage(adapter)` returns a copy of the client bound to different storage. It is the idiomatic way to reuse one configuration across requests, each with its own cookie jar:

```ts theme={null}
const base = { publishableKey }
export const shopkitFor = (request: Request) =>
  createShopkitClient(base).withStorage(createRequestCookieStorage(request))
```

The copy drops any `cookies` accessor, so the adapter you pass is the one used.

<Tip>
  Create **one client per app in the browser and one per request on the server**. Clients are cheap; the cookie accessor belongs to a request.
</Tip>

## Server-rendered cart without a flash

Read the cart on the server and hand it to the browser layer, so the first paint already shows the real basket:

<CodeGroup>
  ```tsx React theme={null}
  // layout.tsx (server component)
  const cart = await shopkit.cart.current().catch(() => null)
  return <ShopkitProvider config={config} initialCart={cart}>{children}</ShopkitProvider>
  ```

  ```js Web components theme={null}
  document.querySelector("qb-shop").initialCart = cartFromServer
  ```
</CodeGroup>

## Cookie attributes

| Attribute | Default | Why |
| - | - | - |
| `path` | `/` | The id must be visible to every route |
| `sameSite` | `lax` | The shopper returns from the hosted checkout with a top-level GET, which `lax` allows and `strict` would drop |
| `secure` | `true` on https origins | So localhost works over http. Pass it explicitly on the server |
| `httpOnly` | Off | Turn it on only if browser code never needs the cart id |
| `maxAge` | From the TTL | Fractional values are floored |
| `domain` | Host-only | Set the parent domain on both sides when the storefront and the return live on different subdomains |

An empty stored value is treated as absent everywhere.

## Bring your own adapter

Any object with `kind`, `get`, `set` and `remove` works, and each may return a promise. Useful for a signed cookie, a Redis or KV-backed session, or a test double:

```ts theme={null}
import type { StorageAdapter } from "@quickbutik/kit"

const redisStorage: StorageAdapter = {
  kind: "redis",
  get: (key) => redis.get(`${sessionId}:${key}`),
  set: (key, value, options) =>
    redis.set(`${sessionId}:${key}`, value, { EX: options?.ttlSeconds }),
  remove: (key) => redis.del(`${sessionId}:${key}`),
}

createShopkitClient({ publishableKey, storage: redisStorage })
```

Adapters that cannot expire values can ignore `ttlSeconds`.

## Exported helpers

```ts theme={null}
import {
  detectRuntime, isBrowser, hasDocumentCookies, hasLocalStorage,
  createMemoryStorage, createLocalStorage, createDocumentCookieStorage,
  createCookieStorage, createRequestCookieStorage, resolveStorage,
  parseCookieHeader, serializeCookie,
  readDocumentCookie, writeDocumentCookie, deleteDocumentCookie,
  SessionStore,
} from "@quickbutik/kit"
```

`isBrowser()` requires a real `window` **and** `document`, so a web worker is not a browser here. `hasLocalStorage()` probes with a real write, because Safari private mode exposes `localStorage` and throws on use.

See [Utilities](/kit/reference/utilities) for signatures.


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