Catalog Cart
Client-only shopping cart for the public storefront, persisted in
localStorageand submitted as a JSON payload at checkout. Audience: new dev with Node/React experience, no Eziseller context.
1. Overview
The catalog cart is a pure client-side construct. There is no cart table in Postgres, no server session, and no "cart API". A React context (CartProvider) holds the item list in component state and mirrors it to localStorage under the key catalog_cart. The cart exists only in the shopper's browser until checkout, at which point the items array is POSTed to the backend as part of the order payload. The backend re-fetches products and re-prices everything from the database — the cart's price field is display-only. On a successful order response the frontend calls clearCart(), which empties state and (via the persistence effect) wipes localStorage.
2. Architecture
The provider is mounted once inside the catalog layout; every storefront page and component reads from it via useCart(). The backend never sees the cart until the final POST.
3. Data model
The cart is not a Prisma model. It is a TypeScript shape kept in memory and JSON-serialised to localStorage.
interface CartItem {
productId: string;
variantId?: string; // optional — products without variants omit this
productName: string; // snapshot, display only
variantName?: string; // snapshot, display only
quantity: number;
price: number; // snapshot at add-time, NOT trusted by BE
imageUrl?: string;
}Identity of a line is (productId, variantId) — adding the same pair merges quantities. Derived values itemCount and totalAmount are recomputed on every render.
The checkout POST strips everything except productId, variantId, quantity before sending — see CheckoutPageClient.tsx:L94-L98.
4. Key flows
4.1 Add → persist → checkout → clear
4.2 Update / remove
updateQuantity(productId, variantId, qty) with qty <= 0 delegates to removeItem, otherwise patches the matching line. Both paths trigger the same useEffect that writes catalog_cart to localStorage — there is no debounce, every keystroke on a quantity input writes synchronously.
4.3 Hydration
On first mount CartProvider reads localStorage.getItem('catalog_cart') and seeds state. There is a one-render window where items === [] before hydration completes; components that render server-side will briefly show an empty cart badge before the client effect fires.
5. Key files
- src/contexts/CartContext.tsx:L1-L110 — provider, reducer-ish handlers, localStorage sync,
useCarthook. - src/components/catalog/storefront/CartDrawer.tsx — slide-out cart used from the catalog header.
- src/components/catalog/storefront/CartButton.tsx — header badge showing
itemCount. - src/app/catalog/[slug]/cart/_components/CartPageClient.tsx — full cart page with qty editors.
- src/app/catalog/[slug]/checkout/_components/CheckoutPageClient.tsx:L37,L94-L106,L158-L173 — reads
items, POSTs to checkout, callsclearCart()on success. - src/app/catalog/[slug]/p/[productSlug]/_components/ProductPageClient.tsx — add-to-cart entry point with variant selection.
- backend/routes/public-catalog.ts:L414-L510 —
POST /:slug/checkout, re-prices items from DB, ignores clientprice.
6. Env vars & config
None. The cart is a pure browser feature; it has no env vars, no feature flag, and no backend configuration.
7. Gotchas & troubleshooting
- localStorage key is global, not per-store. The key
catalog_cartis hard-coded in CartContext.tsx:L32,L40. If a shopper visitsstore-athenstore-bin the same browser, they see store-a's items in store-b's cart. Checkout will fail withProduct not found: ...because the productIds belong to the other store's owner. Fix would be to key by catalog slug. - Cart survives forever. There is no TTL and no "session" concept. A shopper who abandoned a cart six months ago still sees it on return — with stale
priceand possibly deleted products. - Stored
priceis stale. The cart snapshots price at add-time. If the seller raises the price, the shopper keeps seeing the old price in the drawer, cart page, and checkout summary. The backend re-prices, so the finaltotalAmountreturned by/checkoutcan differ from what the UI showed — the success screen reads BE'stotalAmountso it self-corrects, but there is no "price changed" warning. - No server validation until checkout. Out-of-stock, unpublished, or deleted products stay in the cart silently. The first signal is a 400 from the checkout POST (
Some items are out of stock/Product not found). - Variant identity matters. Items are merged on
(productId, variantId). A product without variants hasvariantId === undefined; be careful passingnullvsundefined—findIndexcompares with===and would treat them as distinct lines. - SSR hydration flicker. The provider starts with
items: []and populates on mount. Cart badge briefly shows0on first paint. clearCartonly runs on the success path. If the shopper closes the tab between Razorpay capture and the/checkoutPOST completing, the cart is not cleared and the Razorpay webhook-fallback path (public-catalog.ts:L722+) may create the order server-side, leaving the browser holding a cart for an already-placed order.- No cross-tab sync. Two tabs open on the same catalog each hold their own React state; only the last one to write wins in localStorage, and neither listens to
storageevents.
8. Extension points
- Server-side cart. Introduce a
CartPrisma model keyed by an anonymouscartTokencookie; hydrate the context fromGET /:slug/cart/:tokenand mirror mutations. Enables cross-device carts and price recalculation on read. - Per-store scoping. Change the localStorage key to
catalog_cart:${slug}inside the provider (pass slug via props from the catalog layout). Fixes cross-store contamination without a backend change. - Abandoned-cart recovery. Persist
(cartToken, customerPhone/email, items, updatedAt)server-side and hook WhatsApp/email templates into a scheduler (see cron-and-jobs.md). - Stale-price reconciliation. On cart page mount, call a batch
POST /:slug/cart/refreshthat returns current prices and availability; show a diff banner before the shopper proceeds to checkout. - Stock holds. Reserve inventory for N minutes when an item is added (today stock is only checked at checkout; see products/inventory.md).
9. Related docs
- catalog-overview.md — storefront structure and routing.
- checkout.md — what happens after the cart POSTs.
- ../products/products-and-variants.md — product/variant identity used by cart lines.
- ../products/inventory.md — stock checks during checkout.
- ../orders/order-creation-public-checkout.md — order created from the cart payload.