Skip to main content
RapidDev - Software Development Agency

How to Build a Shopping Cart with Lovable

Build a persistent shopping cart with Lovable and Supabase in 2 hours. You'll get a product catalog with stock Badges, a slide-out cart Sheet, guest and authenticated cart support with merge-on-login via Edge Function, and a complete checkout Form — no code written manually.

What you'll build

  • Product catalog grid with Card layout, stock Badges, and quantity selectors
  • Slide-out cart Sheet with real-time item list, subtotal, and remove buttons
  • Guest cart stored in localStorage that merges with Supabase on login
  • Authenticated cart persisted in Supabase so it survives browser refresh
  • Edge Function that merges guest and authenticated carts on sign-in
  • Checkout Form with shipping address, order summary, and Supabase order creation
  • Order confirmation page with order number and itemized receipt
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate14 min read2–2.5 hoursLovable Pro or higherApril 2026RapidDev Engineering Team
TL;DR

Build a persistent shopping cart with Lovable and Supabase in 2 hours. You'll get a product catalog with stock Badges, a slide-out cart Sheet, guest and authenticated cart support with merge-on-login via Edge Function, and a complete checkout Form — no code written manually.

What you're building

A shopping cart built in Lovable stores product catalog data in Supabase. Guests can add items immediately — their cart lives in localStorage using a guest_id UUID generated on first visit. When the guest signs in or creates an account, a Supabase Edge Function merges their guest cart into their authenticated user cart, so nothing is lost.

Authenticated carts live in Supabase with two tables: carts (one per user) and cart_items (rows linking cart to product with quantity). The cart Sheet slides in from the right and shows the current items, quantity controls, per-item subtotals, and a running total. Stock Badges on product cards update the Add to Cart button state — Out of Stock items are disabled.

Checkout uses a react-hook-form Form with zod validation for shipping address fields. On submit, it creates an order and order_items records in Supabase and clears the cart. The confirmation page shows an auto-generated order number and full itemized summary.

Final result

A fully persistent e-commerce cart that works for both guests and logged-in users, with smooth cart merging and a complete checkout flow.

Tech stack

LovableFrontend
SupabaseDatabase & Edge Functions
Supabase AuthAuth
shadcn/uiUI Components
react-hook-form + zodCheckout Form
Tailwind CSSStyling

Prerequisites

  • Lovable Pro account (the cart merge Edge Function needs enough credits to generate)
  • Supabase project created at supabase.com (free tier works)
  • Supabase URL and anon key ready for Cloud tab → Secrets
  • A product list with names, prices, and stock quantities to seed the database
  • Optional: product images hosted somewhere (Supabase Storage or a public URL)

Build steps

1

Set up the database schema for products and carts

Start with a comprehensive schema prompt covering products, carts, cart_items, and orders. Lovable will generate the SQL migrations and TypeScript types. After generation, add your Supabase credentials to Cloud tab → Secrets.

prompt.txt
1Create a shopping cart application with Supabase. Set up these tables:
2
3- products: id, name, description, price (numeric), image_url, stock_quantity (int), category, slug, created_at
4- carts: id, user_id (nullable, references auth.users), guest_id (text, nullable), created_at, updated_at
5- cart_items: id, cart_id (references carts), product_id (references products), quantity (int), created_at
6- orders: id, user_id (references auth.users), status (pending|paid|shipped|delivered|cancelled), shipping_name, shipping_email, shipping_address, shipping_city, shipping_state, shipping_zip, subtotal, total, created_at
7- order_items: id, order_id, product_id, quantity, unit_price, created_at
8
9RLS rules:
10- products: SELECT public (everyone can read), INSERT/UPDATE/DELETE admin only
11- carts: users can only access their own cart (user_id = auth.uid() OR guest_id = requesting guest_id)
12- cart_items: access through the parent cart RLS
13- orders: users can read/write their own orders only

Pro tip: Ask Lovable to seed the products table with 6 sample products including different categories, prices, and some with low stock (e.g. stock_quantity = 2) so you can test the stock Badge and disabled button behavior.

Expected result: Lovable generates all five tables, TypeScript types, and the Supabase client. The preview shows a basic app shell with navigation.

2

Build the product catalog grid

Ask Lovable to render the products table as a responsive grid of Cards. Each card shows the product image, name, price, category Badge, and stock indicator. The Add to Cart button is disabled when stock_quantity is 0.

prompt.txt
1Build a product catalog page at src/pages/Shop.tsx.
2
3Requirements:
4- Fetch all products from Supabase ordered by name
5- Render a responsive grid: 1 column mobile, 2 tablet, 3 desktop, 4 large desktop
6- Each product is a shadcn/ui Card with:
7 - Product image (aspect-square, object-cover)
8 - Category Badge (variant='secondary')
9 - Product name (font-semibold)
10 - Price formatted as currency ($)
11 - Stock Badge: 'In Stock' (green) if stock_quantity > 5, 'Low Stock' (orange) if 1-5, 'Out of Stock' (red) if 0
12 - Quantity selector (Input type number, min 1, max stock_quantity)
13 - 'Add to Cart' Button, disabled if stock_quantity = 0
14- Add to Cart calls a useCart hook that handles both guest and authenticated cart logic
15- Show a category filter row above the grid using shadcn/ui Badge buttons

Pro tip: Ask Lovable to add a skeleton loading state for the product grid that shows 8 Card-shaped Skeleton components while the Supabase query resolves.

Expected result: A responsive product grid appears with Cards, stock Badges, and functional quantity selectors. Adding items to cart triggers the cart Sheet to slide open.

3

Build the cart Sheet with item management

Create a slide-out Sheet component that renders the current cart items, lets users change quantities or remove items, and shows the running subtotal and total.

src/components/cart/CartSheet.tsx
1import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetFooter } from '@/components/ui/sheet'
2import { Button } from '@/components/ui/button'
3import { Badge } from '@/components/ui/badge'
4import { Separator } from '@/components/ui/separator'
5import { Trash2, Plus, Minus } from 'lucide-react'
6import { useCart } from '@/hooks/useCart'
7
8export function CartSheet() {
9 const { isOpen, setIsOpen, items, updateQuantity, removeItem, subtotal, itemCount } = useCart()
10
11 const formatted = (n: number) =>
12 new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(n)
13
14 return (
15 <Sheet open={isOpen} onOpenChange={setIsOpen}>
16 <SheetContent className="w-[400px] sm:w-[480px] flex flex-col">
17 <SheetHeader>
18 <SheetTitle className="flex items-center gap-2">
19 Cart
20 {itemCount > 0 && (
21 <Badge variant="secondary">{itemCount} {itemCount === 1 ? 'item' : 'items'}</Badge>
22 )}
23 </SheetTitle>
24 </SheetHeader>
25 <div className="flex-1 overflow-y-auto py-4 space-y-4">
26 {items.length === 0 ? (
27 <p className="text-center text-muted-foreground py-12">Your cart is empty</p>
28 ) : (
29 items.map((item) => (
30 <div key={item.id} className="flex gap-3">
31 <img src={item.product.image_url} alt={item.product.name} className="w-16 h-16 rounded-md object-cover" />
32 <div className="flex-1 min-w-0">
33 <p className="text-sm font-medium truncate">{item.product.name}</p>
34 <p className="text-sm text-muted-foreground">{formatted(item.product.price)}</p>
35 <div className="flex items-center gap-2 mt-2">
36 <Button variant="outline" size="icon" className="h-6 w-6" onClick={() => updateQuantity(item.id, item.quantity - 1)}>
37 <Minus className="h-3 w-3" />
38 </Button>
39 <span className="text-sm w-6 text-center">{item.quantity}</span>
40 <Button variant="outline" size="icon" className="h-6 w-6" onClick={() => updateQuantity(item.id, item.quantity + 1)}>
41 <Plus className="h-3 w-3" />
42 </Button>
43 </div>
44 </div>
45 <div className="text-right">
46 <p className="text-sm font-medium">{formatted(item.product.price * item.quantity)}</p>
47 <Button variant="ghost" size="icon" className="h-7 w-7 mt-1" onClick={() => removeItem(item.id)}>
48 <Trash2 className="h-3 w-3 text-muted-foreground" />
49 </Button>
50 </div>
51 </div>
52 ))
53 )}
54 </div>
55 {items.length > 0 && (
56 <SheetFooter className="flex-col gap-2">
57 <Separator />
58 <div className="flex justify-between text-sm font-medium">
59 <span>Subtotal</span>
60 <span>{formatted(subtotal)}</span>
61 </div>
62 <Button className="w-full" onClick={() => { setIsOpen(false) }}>Proceed to Checkout</Button>
63 </SheetFooter>
64 )}
65 </SheetContent>
66 </Sheet>
67 )
68}

Expected result: The cart Sheet slides in from the right. Items show with quantity controls and a remove button. Subtotal updates immediately when quantities change.

4

Build the guest-to-auth cart merge Edge Function

When a guest user signs in, their localStorage cart needs to merge with any existing authenticated cart. A Supabase Edge Function handles this securely: it finds both carts, merges items by adding quantities for duplicate products, and clears the guest cart.

prompt.txt
1Create a Supabase Edge Function at supabase/functions/merge-cart/index.ts that merges a guest cart into the authenticated user's cart.
2
3The function receives: { guest_id: string } in the request body.
4The Authorization header contains the user's JWT (authenticated request).
5
6Logic:
71. Extract user ID from the JWT using Supabase Auth
82. Find the guest cart where guest_id = input guest_id
93. Find or create the user's cart where user_id = auth user id
104. For each item in the guest cart:
11 - If a cart_item with the same product_id exists in the user's cart, add the quantities
12 - Otherwise, insert the cart_item into the user's cart
135. Delete all items from the guest cart
146. Delete the guest cart record
157. Return { success: true, merged_items: count }
16
17Use the Supabase service role key (from Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')) so the function can access both carts regardless of RLS.

Pro tip: In the frontend useCart hook, call this Edge Function from a useEffect that runs when the auth state changes from null to a user session. Pass the guest_id from localStorage.

Expected result: Signing in with items in the guest cart calls the Edge Function. After merge, the authenticated cart contains all guest items. Refreshing the page still shows the merged cart.

5

Build the checkout form and order creation

Ask Lovable to create the checkout page with a react-hook-form + zod form for shipping details, an order summary sidebar, and a submit handler that creates the order in Supabase and clears the cart.

prompt.txt
1Build a checkout page at src/pages/Checkout.tsx.
2
3Requirements:
4- Two-column layout: checkout form (left) and order summary (right)
5- Form fields using react-hook-form + zod:
6 - Full name (required, min 2 chars)
7 - Email (required, valid email)
8 - Address line 1 (required)
9 - City (required)
10 - State (required, 2-letter code)
11 - ZIP code (required, 5 digits)
12- Order summary shows: each cart item with quantity and price, subtotal, estimated tax (8%), and total
13- On submit:
14 1. Insert into orders table with all shipping fields, subtotal, and total
15 2. For each cart item, insert into order_items with product_id, quantity, unit_price
16 3. Delete all cart_items for the current cart
17 4. Decrease each product's stock_quantity by the ordered quantity
18 5. Navigate to /order-confirmation/[orderId]
19- Show a loading spinner on the Submit button while the Supabase calls complete
20- If stock is insufficient at submission time, show a toast error and highlight the affected item

Expected result: The checkout page shows the form and order summary side by side. Submitting a valid form creates an order in Supabase, clears the cart, and navigates to the confirmation page.

Complete code

src/hooks/useCart.ts
1import { useState, useEffect, useCallback } from 'react'
2import { supabase } from '@/integrations/supabase/client'
3import { useAuth } from '@/hooks/useAuth'
4
5type CartItem = {
6 id: string
7 quantity: number
8 product: { id: string; name: string; price: number; image_url: string; stock_quantity: number }
9}
10
11const GUEST_ID_KEY = 'cart_guest_id'
12
13function getGuestId(): string {
14 let id = localStorage.getItem(GUEST_ID_KEY)
15 if (!id) {
16 id = crypto.randomUUID()
17 localStorage.setItem(GUEST_ID_KEY, id)
18 }
19 return id
20}
21
22export function useCart() {
23 const { user } = useAuth()
24 const [items, setItems] = useState<CartItem[]>([])
25 const [isOpen, setIsOpen] = useState(false)
26 const [cartId, setCartId] = useState<string | null>(null)
27
28 const fetchCart = useCallback(async () => {
29 const query = user
30 ? supabase.from('carts').select('id').eq('user_id', user.id).single()
31 : supabase.from('carts').select('id').eq('guest_id', getGuestId()).single()
32
33 const { data: cart } = await query
34 if (!cart) return
35 setCartId(cart.id)
36
37 const { data } = await supabase
38 .from('cart_items')
39 .select('id, quantity, product:products(id, name, price, image_url, stock_quantity)')
40 .eq('cart_id', cart.id)
41 setItems((data as unknown as CartItem[]) ?? [])
42 }, [user])
43
44 useEffect(() => { fetchCart() }, [fetchCart])
45
46 const addItem = async (productId: string, quantity: number) => {
47 if (!cartId) return
48 const existing = items.find((i) => i.product.id === productId)
49 if (existing) {
50 await supabase.from('cart_items').update({ quantity: existing.quantity + quantity }).eq('id', existing.id)
51 } else {
52 await supabase.from('cart_items').insert({ cart_id: cartId, product_id: productId, quantity })
53 }
54 await fetchCart()
55 setIsOpen(true)
56 }
57
58 const removeItem = async (itemId: string) => {
59 await supabase.from('cart_items').delete().eq('id', itemId)
60 setItems((prev) => prev.filter((i) => i.id !== itemId))
61 }
62
63 const updateQuantity = async (itemId: string, quantity: number) => {
64 if (quantity < 1) return removeItem(itemId)
65 await supabase.from('cart_items').update({ quantity }).eq('id', itemId)
66 setItems((prev) => prev.map((i) => i.id === itemId ? { ...i, quantity } : i))
67 }
68
69 const subtotal = items.reduce((sum, i) => sum + i.product.price * i.quantity, 0)
70 const itemCount = items.reduce((sum, i) => sum + i.quantity, 0)
71
72 return { items, isOpen, setIsOpen, cartId, addItem, removeItem, updateQuantity, subtotal, itemCount }
73}

Customization ideas

Stripe payment integration

Add a Supabase Edge Function that creates a Stripe Checkout Session from the order data. Redirect the user to Stripe's hosted checkout page. On payment success, Stripe sends a webhook to another Edge Function that updates the order status to 'paid' and triggers a confirmation email.

Wishlist feature

Add a wishlists table linked to the user. Product cards get a heart icon button that toggles the item in the wishlist. A /wishlist page shows saved products with an Add to Cart button for each. The wishlist persists across sessions for authenticated users.

Product reviews and ratings

Add a reviews table (id, product_id, user_id, rating 1–5, body, created_at) with RLS allowing users to write one review per product they have ordered. Display average rating as a star rating component below the product name on the catalog page.

Discount code system

Add a discount_codes table (code, type: percent|fixed, value, min_order, max_uses, uses_count, expires_at). Add a coupon input field at checkout that validates the code via a Supabase Edge Function and applies the discount to the order total before submission.

Inventory alerts for admins

Add a Supabase scheduled Edge Function that runs daily and queries products where stock_quantity is below 10. It sends an email digest to an admin address via Resend listing products that need restocking, with product names and current quantities.

Common pitfalls

Pitfall: Letting the anon key bypass RLS on the carts table

How to avoid: Add a SELECT policy on carts: user_id = auth.uid(). For guest carts, rely on the guest_id matching a value only the client knows. The Edge Function for merging should use the service role key stored in Secrets, never exposed to the client.

Pitfall: Not handling the case where a user signs in on a device with no guest cart

How to avoid: Wrap the Edge Function call in a try-catch. If no guest cart exists, return early with { success: true, merged_items: 0 }. In the frontend, only call the merge function if localStorage contains a guest_id and the local cart has at least one item.

Pitfall: Not decrementing stock_quantity in a transaction

How to avoid: Ask Lovable to use a Supabase RPC function for the stock decrement that runs UPDATE products SET stock_quantity = stock_quantity - qty WHERE id = product_id AND stock_quantity >= qty RETURNING id. If no row is returned, the item is out of stock and the checkout should be rejected.

Pitfall: Storing the full cart in a React context without syncing back to Supabase

How to avoid: Treat Supabase as the source of truth for authenticated users. Every addItem, removeItem, and updateQuantity call should update Supabase first, then refetch. Use TanStack Query with invalidateQueries to keep the UI in sync.

Best practices

  • Generate the guest_id with crypto.randomUUID() on first page load and persist it in localStorage. Never send it as a URL parameter — keep it client-side only.
  • Use a single useCart hook that abstracts the guest vs authenticated logic. The rest of the app only calls addItem, removeItem, and updateQuantity without knowing the underlying storage mechanism.
  • Always validate stock availability server-side in the checkout submission, even if you check it client-side on the product page. Stock can change between page load and checkout.
  • Run the cart merge Edge Function with the service role key so it can access both the guest cart and the user cart regardless of RLS. Never expose the service role key to the browser.
  • Add optimistic UI updates in the cart Sheet for quantity changes. Update the local state immediately and revert if the Supabase update fails, so the UX feels instant.
  • Store unit_price in order_items at the time of purchase, not a reference to the current product price. Product prices can change, but historical order prices must be immutable.

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a shopping cart in Lovable (React + Supabase). I have tables: products, carts (user_id nullable, guest_id text), cart_items (cart_id, product_id, quantity). Write a TypeScript useCart hook that handles both guest (localStorage guest_id) and authenticated (Supabase) carts. Include addItem, removeItem, updateQuantity functions and a subtotal computed value. When the user signs in, call a merge-cart Edge Function with the guest_id. Handle loading and error states.

Lovable Prompt

Add a Recently Viewed section at the bottom of each product page. Track the last 6 viewed product IDs in localStorage. Fetch those products from Supabase and render them as a horizontal scrollable row of small Cards showing the product image, name, and price. Add a 'Clear History' button. Do not show the current product in the recently viewed list.

Build Prompt

In Supabase, write a PostgreSQL function called checkout_cart(p_cart_id uuid, p_order_id uuid) that runs inside a transaction. It should: 1) Fetch all cart_items for p_cart_id with their product prices. 2) For each item, check stock_quantity >= quantity, raising an exception if not. 3) Decrement stock_quantity for each product. 4) Insert order_items rows for p_order_id. 5) Delete all cart_items for p_cart_id. Return the total number of items processed.

Frequently asked questions

What happens to the guest cart if the user never logs in?

The guest cart persists in Supabase as long as the guest_id exists in localStorage. It will remain until the user clears their browser data or you add a scheduled Supabase Edge Function that deletes guest carts older than 30 days. Ask Lovable to set up that cleanup function.

Can I add Stripe payments to this cart?

Yes. After the order is created in Supabase, call a Supabase Edge Function that creates a Stripe Checkout Session using the order items and total. The function returns a session URL and the frontend redirects the user to Stripe's hosted page. On success, Stripe calls a webhook Edge Function that updates the order status to paid. Store your Stripe keys in Cloud tab → Secrets.

Does the cart work on mobile?

Yes. The Sheet component from shadcn/ui is mobile-optimized and slides in from the bottom on small screens when configured with side='bottom'. Ask Lovable to add side='bottom' on mobile using a responsive Tailwind check, and side='right' on desktop. The product grid's responsive classes handle column count automatically.

Can two users add the same last item simultaneously?

The client-side check on stock_quantity is optimistic and can be raced. To prevent overselling, ask Lovable to create a Supabase RPC function for the checkout step that uses a conditional UPDATE (WHERE stock_quantity >= qty) inside a transaction. If the update returns 0 rows, the checkout should be rejected with a clear error message.

How do I add product images to Supabase Storage?

In your Supabase dashboard, go to Storage → New Bucket → name it 'products' → make it public. Upload images there and copy the public URL into the image_url field of your products table. Ask Lovable to add an image upload feature to your admin product management page that handles the file upload to this bucket automatically.

Can I deploy this as a full e-commerce site?

Yes. Click the Publish icon in Lovable's top-right corner to get a public URL. For a custom domain, go to Publish → Settings. For production e-commerce, make sure you have added proper RLS policies, Stripe webhook verification in your Edge Function, and HTTPS (Lovable provides this automatically). Consider also adding a robots.txt and sitemap for SEO.

Is there help available if I need custom cart logic or third-party integrations?

RapidDev builds production-grade Lovable apps including e-commerce stores with complex cart rules, multi-currency support, and payment provider integrations. Reach out if you need hands-on help.

How do I show order history to logged-in users?

Ask Lovable to add an /orders page that fetches from the orders table where user_id = auth.uid(). Show each order as an Accordion: the summary row shows order number, date, status Badge, and total. Expanding it reveals the order_items joined with products showing name, quantity, and unit price.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.