Cart & Wishlist
Last updated on 2026-08-30
The cart and wishlist use a hybrid storage strategy: guests get localStorage, authenticated users get database persistence, and items merge automatically when a guest logs in.
Hybrid Cart
How It Works
The CartProvider in context/cart-context.tsx detects auth state and delegates to the appropriate storage:
Guest user:
→ Read/write localStorage ("ember-guest-cart")
→ Cart items are CartItem[] in React state
Authenticated user:
→ Read/write Supabase cart_items table
→ Server-side persistence via Supabase queries
Guest logs in:
→ Guest cart merges into server cart
→ Quantities take the higher value
→ localStorage is cleared
Cart Context API
const {
items, // CartItem[] — current cart items
count, // number — total item count
subtotal, // number — total price
addItem, // (product, quantity?, variants?) => void
removeItem, // (itemId) => void
updateQuantity, // (itemId, quantity) => void
clear, // () => void
loading, // boolean — initial load state
} = useCart()
Guest Cart (localStorage)
Guest cart items are stored as:
interface GuestCartItem {
productId: string
product: Product // Full product object for display
quantity: number
selectedVariants: { type: string; value: string }[]
}
Item IDs follow the pattern guest-{productId} to distinguish from database UUIDs.
Authenticated Cart (Supabase)
For logged-in users, cart operations use Supabase directly:
- Add item: Check if product exists in cart. If yes, increment quantity. If no, insert new row.
- Update quantity: Update the
quantitycolumn on the cart item row. - Remove item: Delete the row by ID.
- Clear cart: Delete all rows for the user.
The cart_items table schema:
cart_items (
id uuid PRIMARY KEY,
user_id uuid REFERENCES auth.users,
product_id uuid REFERENCES products,
quantity integer,
variant_selections jsonb -- [{type: "size", value: "M"}, ...]
)
Cart Merge on Login
When a guest user logs in, the mergeGuestCartToServer function runs:
- Read guest cart from localStorage
- Fetch existing server cart for dedup
- For each guest item:
- If product already in server cart: take the higher quantity
- If product not in server cart: insert it
- Clear localStorage
This happens automatically in the useEffect that watches auth state transitions.
Hybrid Wishlist
The wishlist follows the same pattern as the cart.
Wishlist Context API
const {
items, // WishlistItem[] — wishlisted products
count, // number — total count
isInWishlist, // (productId) => boolean
toggle, // (product) => void — add or remove
remove, // (productId) => void
loading, // boolean
} = useWishlist()
Guest Wishlist (localStorage)
Stored under "ember-guest-wishlist" with product ID and full product data.
Authenticated Wishlist (Supabase)
The wishlists table:
wishlists (
id uuid PRIMARY KEY,
user_id uuid REFERENCES auth.users,
product_id uuid REFERENCES products,
created_at timestamptz
)
-- Unique constraint on (user_id, product_id)
Toggle checks for existing row: if found, deletes it; if not, inserts.
Wishlist Merge on Login
Same as cart merge: guest wishlist items are inserted (with dedup) into the server wishlist, then localStorage is cleared.
Recently Viewed
The RecentlyViewedProvider in context/recently-viewed-context.tsx is client-side only (localStorage). It tracks the last 12 products viewed and is not affected by auth state.
Using the Cart in Checkout
The checkout page reads from useCart() to build the Stripe Checkout Session:
const { items } = useCart()
// Maps cart items to the API request format
const response = await fetch("/api/checkout", {
method: "POST",
body: JSON.stringify({
items: items.map((item) => ({
product_id: item.product.id,
name: item.product.name,
price: item.product.salePrice ?? item.product.price,
quantity: item.quantity,
})),
email: user?.email,
}),
})
After successful payment, the Stripe webhook clears the authenticated user's cart from the database.