Skip to Content

Cart

The cart lives entirely in a cookie while the customer shops — no database rows are written until checkout starts. Prices, tax, stock, and automatic promotions are always resolved server-side when the cart is read.

The codec lives in src/lib/server/cart-cookie.ts. The cookie is named cart and holds a compact JSON array of [version, [variantId, qty], ...]:

src/lib/server/cart-cookie.ts
export const CART_COOKIE = "cart"; // e.g. [1, [12, 2], [34, 1]] — version 1, variant 12 ×2, variant 34 ×1 export function parseCartCookie(raw: string | undefined | null): CartLine[]; export function serializeCartCookie(lines: CartLine[]): string;

The cookie is httpOnly, sameSite: "lax", and lives 30 days. It is intentionally unsigned — it only holds variant ids and quantities, both of which the customer controls through the UI anyway. Carts are capped at 50 different items and quantity 999 per line.

There is no cart merge on login. The cookie is the cart regardless of auth state — signing in just changes how prices (group pricing) and tax exemption are resolved when the cart is read.

Read Model

getCartView turns cookie lines into a priced view in a handful of batched queries. It never writes to the database.

src/lib/server/services/cart.ts
const view = await getCartView(cartLines, customerId, { skipPromotions }); // view: { lines, itemCount, subtotal, discount, taxTotal, total, isTaxExempt, promotions }

It resolves, per line: the featured image (variant image → product asset → featured/first variant image), group pricing for logged-in customers, available stock (variant stock minus active reservations), tax, and any qualifying automatic promotions.

Remote Functions

The UI talks to the cart through remote functions in src/lib/remote/cart.remote.ts. getCart is a query(); each mutating command() is valibot-validated, keyed by variantId, rewrites the cookie, then single-flight refreshes getCart:

src/lib/remote/cart.remote.ts
export const getCart = query(/* ... */); // → CartView export const addToCart = command(schema, /* ... */); // { variantId, quantity } export const setCartQuantity = command(schema, /* ... */); // quantity 0 removes the line export const removeCartLine = command(schema, /* ... */); // { variantId }

addToCart and setCartQuantity check available stock against reservations; addToCart errors when it would exceed stock, while setCartQuantity silently clamps to what is purchasable. When a checkout draft is active, removing a line (or emptying the cart) releases that draft’s stock reservations immediately instead of waiting for the 15-minute expiry.

UI

src/lib/components/storefront/CartSheet.svelte renders getCart().current. Mutations go through src/lib/cart-actions.ts, which applies an optimistic override (src/lib/cart-optimistic.ts) so the line updates in the same frame as the click; the command’s single-flight refresh then replaces it with the server-computed cart, and errors roll the override back. Optimistic math is structural only (lines, quantities, line totals); cart-level money (subtotal, discount, VAT, total) waits for the server. The cartStore in src/lib/stores/cart.svelte.ts holds sheet open/close state plus an in-flight counter (isBusy); the cart data itself comes from the remote query.

Checkout

The first database write happens when checkout starts, in orderService.startCheckout (src/lib/server/services/orders.ts). It snapshots the cookie lines into an order (active=true, state=created) with prices/names from getCartView and creates 15-minute stock reservations. The draft is identified by the checkout_token cookie. See Orders for the full lifecycle.

Last updated on