Skip to main content

What the kit remembers

Four opaque handles and one preference, never prices, personal data or credentials: 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. 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. 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 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:
1

A cookies accessor was supplied

Server cookie storage. Supplying one always wins.
2

Not a browser

Memory.
3

document.cookie is usable

Document cookies.
4

localStorage is usable

localStorage.
5

Otherwise

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

Next.js App Router

A server component cannot set cookies and a server action can, so use two clients:
lib/shopkit.ts
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.

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:
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:
The copy drops any cookies accessor, so the adapter you pass is the one used.
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.

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:
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:
Adapters that cannot expire values can ignore ttlSeconds.

Exported helpers

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 for signatures.