# How to Add a Wish List to Your App (Copy-Paste Prompts Included)

- Tool: App Features
- Last updated: July 2026

## TL;DR

A wish list needs a heart-toggle button with optimistic UI, a Supabase table with a UNIQUE constraint to prevent duplicates, a dedicated list page, and a share-link flow. With Lovable or V0 you can ship this in 1–2 hours for $0–$25/month. The two pieces AI tools consistently miss are merging the guest localStorage list on login and deduplicating price-drop notification emails.

## What a Wish List Feature Actually Is

A wish list lets shoppers bookmark products they want without committing to a purchase — the digital equivalent of a sticky note on a shelf. Single-tap save from a product page, a dedicated list to revisit later, and the ability to share it with others are the core three behaviours. Beyond those, the feature earns its keep through price-drop notifications that pull users back to convert, and a guest-to-logged-in merge flow that prevents the frustration of a cleared list after sign-up. The data model is deliberately simple — one join table — but the edge cases around guest sessions, share-link privacy, and notification deduplication are where first builds fall apart.

## Anatomy of the Feature

Five components, all of which AI tools can generate — the heart toggle and list page are routine; the share link and price-drop cron are where explicit prompting is required.

- **Heart / save toggle button** (ui): A shadcn/ui Button with a Heart icon from lucide-react. On click it fires an optimistic UI update — fills the heart immediately in local state — then inserts or deletes the wish_list_items row in Supabase in the background. Reverting the optimistic state on error keeps the UI honest.
- **Wish list page** (ui): A product grid rendering saved items fetched from wish_list_items joined with the products table. Requires an empty state with a 'Start exploring' CTA that links to the product catalogue. Supports sort by date added (default) and price, and individual item removal.
- **wish_list_items table** (data): Supabase PostgreSQL table with columns user_id, product_id, saved_price_cents, and added_at. A UNIQUE(user_id, product_id) constraint prevents duplicates at the database level. RLS policies restrict SELECT/INSERT/DELETE to the row owner.
- **Share link backend** (backend): A shared_wish_lists table stores a share_token (UUID generated by gen_random_uuid()) per user. A public Supabase Edge Function or Next.js API route accepts GET /wish-list/share/[token] and returns product_id, name, price, and image_url — never user_id or email. The public RLS policy allows SELECT on wish_list_items WHERE a valid share_token matches.
- **Price drop notification cron** (backend): A scheduled Supabase Edge Function runs daily or hourly, compares products.price_cents against wish_list_items.saved_price_cents for all active wish list rows, and sends an email via Resend for each price decrease. The last_notified_at column prevents repeated emails for items that stay at their lowered price.

## Data model

Three tables cover wish list persistence, sharing, and price-drop deduplication. Paste this into the Supabase SQL editor and run it before prompting your AI tool.

```sql
create table public.wish_list_items (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade not null,
  product_id uuid references public.products(id) on delete cascade not null,
  saved_price_cents int not null,
  last_notified_at timestamptz,
  added_at timestamptz not null default now(),
  constraint wish_list_items_unique_user_product unique (user_id, product_id)
);

alter table public.wish_list_items enable row level security;

create policy "Users can view own wish list"
  on public.wish_list_items for select
  using (auth.uid() = user_id);

create policy "Users can insert own wish list items"
  on public.wish_list_items for insert
  with check (auth.uid() = user_id);

create policy "Users can delete own wish list items"
  on public.wish_list_items for delete
  using (auth.uid() = user_id);

create index wish_list_user_idx
  on public.wish_list_items (user_id, added_at desc);

-- Share link table
create table public.shared_wish_lists (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade not null unique,
  share_token uuid not null default gen_random_uuid(),
  is_public boolean not null default true,
  created_at timestamptz not null default now()
);

alter table public.shared_wish_lists enable row level security;

create policy "Users manage own share settings"
  on public.shared_wish_lists for all
  using (auth.uid() = user_id)
  with check (auth.uid() = user_id);

-- Allow public read on wish_list_items via share token (join in Edge Function)
create policy "Public share read on wish list"
  on public.wish_list_items for select
  using (
    exists (
      select 1 from public.shared_wish_lists s
      where s.user_id = wish_list_items.user_id
        and s.is_public = true
    )
  );
```

The UNIQUE constraint on (user_id, product_id) is the safety net for rapid double-taps. The last_notified_at column on wish_list_items drives price-drop notification deduplication — only notify when the price drops further than the last notified price.

## Build paths

### Lovable — fit 5/10, 1–2 hours

Easiest full path: Lovable Cloud provisions the Supabase schema and auth, generates the toggle button and list page in one session, and Supabase Realtime keeps heart state in sync across tabs.

1. Create a new Lovable project and connect Lovable Cloud so Supabase auth and the database are provisioned automatically
2. Paste the prompt below and let Agent Mode build the heart toggle component, wish list page, and guest localStorage merge on auth
3. Open the Cloud tab and confirm the wish_list_items table and its RLS policies were created
4. Click Publish and test the share link on a separate incognito browser window to verify the public read policy works

Starter prompt:

```
Build a wish list feature for an e-commerce app. Use Supabase for persistence. Heart toggle button: shadcn/ui Button with Heart icon (lucide-react); on click perform an optimistic UI update (fill the heart immediately) then insert or delete a row in the wish_list_items table (user_id, product_id, saved_price_cents, added_at). Add a UNIQUE(user_id, product_id) constraint and handle the 23505 unique violation error gracefully by treating it as 'already saved'. For logged-out users, store wish list in localStorage as an array of product_id strings; on auth state change (Supabase onAuthStateChange), batch-insert the localStorage items into wish_list_items and clear localStorage. Wish list page: product grid of saved items (join wish_list_items with products); empty state with 'Start exploring' CTA; sort by date added (default) and price; individual remove button; a 'Move to cart' button per item. Nav icon: show a badge with count of saved items. Share link: create a shared_wish_lists table with a share_token UUID; public Edge Function GET /wish-list/share/[token] returns name, price, image_url only — never user_id. Load the user's existing wish list on page mount and initialise heart state from the result set.
```

Limitations:

- Price-drop notification cron must be scheduled manually in the Supabase Dashboard after Lovable generates the Edge Function body
- The share link public RLS policy may need manual review — confirm it does not expose user_id or email in the Edge Function response
- Supabase Realtime for multi-tab heart sync works reliably on the published URL; test there rather than the in-editor preview

### V0 — fit 4/10, 1.5–3 hours

Best when the wish list is part of a larger Next.js storefront — Server Actions handle the toggle with clean optimistic updates and the guest cookie-to-DB merge is elegant in App Router.

1. Paste the prompt below; V0 generates the WishListButton, WishListPage, and guest merge Server Actions
2. Add NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY in the Vars panel
3. Run the SQL schema from this page in the Supabase SQL editor to create wish_list_items and shared_wish_lists
4. Publish to Vercel and test the share URL in an incognito tab to confirm the public endpoint returns only safe fields

Starter prompt:

```
Build a wish list feature for a Next.js 14 App Router e-commerce app using Supabase. WishListButton client component: Heart icon (lucide-react) toggles between filled and outline; call a Server Action to insert or delete from wish_list_items (user_id, product_id, saved_price_cents); use useOptimistic to update the heart state before the action resolves; handle UNIQUE constraint violation (23505) by treating it as already-saved. For unauthenticated users, store wish list in a client-side cookie as JSON array of product_ids; on sign-in, call a Server Action that reads the cookie, batch-inserts rows into wish_list_items, then clears the cookie. WishListPage server component at /wish-list: fetch all wish_list_items joined with products for the current user; show product grid with image, name, price; remove button; 'Move to cart' button; empty state with link to /products. Nav badge: read wish list count from Supabase on each request (or use a React context updated after toggle). Share link: Server Action creates or fetches a shared_wish_lists row with a UUID token; public route /wish-list/share/[token] returns product name, price, image_url only. No HTML in text fields.
```

Limitations:

- No auto-provisioning — Supabase schema must be created manually using the SQL from this page
- Guest wish list stored in a cookie has a 4KB size limit — for catalogues with large product_id strings, localStorage on the client side is safer
- SSR hydration mismatch can occur if the heart icon reads from cookie on server and localStorage on client — keep the source of truth consistent

### Custom — fit 3/10, 2–4 days

Overkill for a standard single wish list, but warranted when you need multiple named lists, gift registry collaboration, or merchant-facing wish list analytics.

1. Extend the schema with a wish_lists table (name, user_id, is_gift_registry) and relate wish_list_items to it, not directly to user_id
2. Build a 'claimed by' flow for gift registries: a claimed_items table records which guest claimed which item so duplicates are avoided
3. Add a merchant analytics endpoint aggregating wish list saves and conversion rate per product_id
4. Wire abandoned-wish-list email campaigns via Resend or Klaviyo triggered by items older than 30 days with no purchase

Limitations:

- Not worth the complexity overhead for a straightforward save-for-later feature — AI tools cover that in under 2 hours

## Gotchas

- **Duplicate rows from rapid double-taps** — Without a UNIQUE constraint, a user double-tapping the heart button inserts two rows for the same product before the first DB response arrives. The list then shows the item twice and the second remove tap leaves a phantom row. Fix: Add UNIQUE(user_id, product_id) to wish_list_items in your SQL schema. In the toggle handler, catch PostgreSQL error code 23505 and treat it as 'already saved' rather than an error — update heart state to filled and move on.
- **Guest wish list disappears on login** — AI tools save wish list to localStorage for logged-out users but don't wire the Supabase onAuthStateChange listener that merges those items into the database after sign-in. The user spends time saving items, creates an account, and their list is gone. Fix: In your prompt, explicitly require: 'On onAuthStateChange event when user transitions from null to signed-in, read localStorage wish list, batch-insert all items into wish_list_items via Supabase, then clear localStorage.' Test this flow in a fresh incognito session.
- **Share link exposes user_id and email** — AI-generated public share endpoints often return the full wish_list_items row including user_id, or join with auth.users to fetch display names — leaking account data to anyone with the share URL. Fix: The share Edge Function or API route must return only product_id, name, price_cents, and image_url. Never include user_id, email, or any internal identifier in the public response. Explicitly state this in your prompt.
- **Heart icon shows empty on page refresh** — AI builds often initialise the heart icon from React component state (default: empty) and don't fetch the user's existing wish list on page load. After any navigation or refresh, all hearts appear empty even for saved items. Fix: Fetch the complete list of product_ids in the user's wish_list_items on page mount (or as a server component data fetch), then pass that set to every heart button so it initialises correctly. A loading skeleton during fetch prevents the flash of wrong state.
- **Price-drop notification emails sent on every cron run** — The price-drop cron finds items where products.price_cents < wish_list_items.saved_price_cents and emails the user. Without deduplication, every subsequent cron run re-sends the same notification because the price is still lower than the saved price. Fix: After sending a notification, update last_notified_at and also update saved_price_cents to the new (lower) price. Future cron runs will only trigger again if the price drops further below the newly recorded price.

## Best practices

- Always use optimistic UI for the heart toggle — any visible loading delay on a save action destroys the 'effortless save' feel that makes wish lists sticky
- Set the UNIQUE constraint at the database level, not just in application code — double-tap races and concurrent sessions both bypass app-level guards
- Persist guest wish list to localStorage immediately on every toggle, not just on page unload — mobile browsers kill tabs without firing unload events
- Keep the public share endpoint response to product fields only — treat it like a public API and never include any user identity data
- Deduplicate price-drop notifications by updating last_notified_at and saved_price_cents atomically after each notification send
- Show the wish list item count badge in the nav immediately after any toggle (update in React context or App State) — users rely on the badge as confirmation their save worked
- Include an 'Out of stock' indicator on wish list items rather than silently hiding them — users return specifically to buy items they saved

## Frequently asked questions

### Can I save wish list items for logged-out users?

Yes. Store wish list as an array of product IDs in localStorage (or a cookie) for guests. When the user signs in, read those items and batch-insert them into your Supabase wish_list_items table via the Supabase onAuthStateChange event, then clear localStorage. Explicitly include this merge step in your AI prompt — it's the most commonly skipped piece.

### How do I let users share their wish list with friends?

Create a shared_wish_lists table with a UUID share_token per user. Your public share route (/wish-list/share/[token]) fetches items for that token and returns only product name, price, and image — never user_id or email. The recipient gets a read-only browseable list without needing to log in. Set a public Supabase RLS policy that allows SELECT on wish_list_items when a matching share_token exists.

### How do I send price drop alerts for wish listed items?

Add a saved_price_cents column to wish_list_items to capture the price at save time. A scheduled Supabase Edge Function (daily or hourly cron, configured in the Supabase Dashboard) compares that against products.price_cents. When a decrease is detected, send an email via Resend with a direct link to the product. Store last_notified_at and update saved_price_cents after each notification to prevent repeat emails.

### How do I prevent the same item being added to the wish list twice?

Add a UNIQUE(user_id, product_id) constraint to the wish_list_items table. When the insert triggers a 23505 unique violation, catch it and treat it as success (heart is already filled). Never rely solely on application-level duplicate checks — rapid taps and concurrent sessions bypass them.

### Can users have multiple wish lists?

Not with the schema above — it's designed for one list per user. For multiple named lists, add a wish_lists table (id, user_id, name) and relate wish_list_items to wish_list_id instead of directly to user_id. This is a meaningful schema change and the most common custom-development trigger for this feature.

### Can wish list items be moved directly to the cart?

Yes — add a 'Move to cart' button on each wish list item. On click, insert the product into your cart table (or update cart state), then optionally remove it from wish_list_items. Include 'Move to cart' button per item on the wish list page in your prompt so the AI tool generates both actions.

### How do I show how many other users have wishlisted a product?

Add a wishlist_count integer column to your products table (or compute it with a COUNT query). Update it via a Supabase database trigger or increment it in your insert/delete Server Action. Display it on the product page as 'Saved by 142 people'. A DB trigger is the most reliable approach — it survives concurrent inserts that would race an application-side count.

### How do I handle wish-listed items that go out of stock?

Add an in_stock boolean or stock_quantity column to your products table. On the wish list page, join against products and render an 'Out of stock' badge on affected items — and optionally send a 'Back in stock' email when inventory is replenished. Never silently hide out-of-stock wish list items; users return specifically to buy things they saved.

---

Source: https://www.rapidevelopers.com/app-features/wish-list
© RapidDev — https://www.rapidevelopers.com/app-features/wish-list
