# How to Add Product Recommendations to Your App (Copy-Paste Prompts Included)

- Tool: App Features
- Last updated: July 2026

## TL;DR

Product recommendations need three pieces: a behaviour tracker that records views and purchases, a recommendation engine (Supabase pgvector + OpenAI text-embedding-3-small for semantic similarity, or rule-based SQL JOINs for simpler catalogues), and a card grid UI that renders results. With Lovable or V0 you can ship a working recommendations widget in 4-8 hours. Running costs start at $0 on Supabase free tier and reach $25-30/mo at 1,000 active users.

## What a Product Recommendations Feature Actually Is

Product recommendations surface items a user is likely to want based on what they and similar users have already viewed, clicked, or bought. The 'you may also like' widget is so common that users now expect it on any product-bearing page — its absence signals an incomplete experience. The engineering reality is two sub-problems: collecting events (views, clicks, purchases) and turning those events into a ranked list. For most early-stage apps, Supabase pgvector with OpenAI embeddings solves both — each item gets an embedding vector, and nearest-neighbour search finds semantically similar items weighted by event frequency. The product decisions that matter are cold-start behaviour for new users, how many results to show, and where on the page the widget lives.

## Anatomy of the Feature

Six components. The recommendation engine and data model are where AI tools need precise prompting — everything else generates reliably.

- **Recommendation engine** (backend): Supabase Edge Function (Deno) that queries pgvector nearest-neighbours on the item_embeddings table or runs SQL frequency analysis on user_item_events and returns a ranked list of item IDs. OpenAI text-embedding-3-small converts item titles and descriptions into 1536-dimension vectors.
- **User behaviour tracker** (data): Records every view, click, add_to_cart, and purchase as a row in user_item_events with user_id, item_id, event_type, and timestamp. This table feeds both the collaborative filtering model and the analytics dashboard.
- **Recommendation API** (backend): Supabase Edge Function endpoint that accepts user_id and item_id (current page context), runs the recommendation logic, and returns up to 12 ranked item objects. Results are cached in recommendations_cache for 1 hour to avoid repeated pgvector queries.
- **Product card grid UI** (ui): React grid using shadcn/ui Card components in a responsive CSS grid (2 columns mobile, 3-4 desktop). Each card shows image, title, price, and an inline 'Add to cart' CTA. Skeleton loading states via shadcn/ui Skeleton render while the API responds.
- **Personalisation store** (data): Two Supabase tables: user_item_events stores raw behavioural signals; item_embeddings stores the pgvector vectors for each catalogue item. RLS on user_item_events ensures each user's event history is private — no cross-user data leakage.
- **Analytics hook** (backend): Impression and click events fired to Supabase from the recommendation widget itself — separate from product page events. Tracks recommendation_click with item_id, position_in_list, and source_item_id so you can measure which recommendations convert.

## Data model

Three tables cover the full recommendations data model. Run this in the Supabase SQL editor (Dashboard → SQL editor → New query):

```sql
-- Enable pgvector extension (one-time, in Supabase Dashboard → Database → Extensions)
-- Search for "vector" and toggle on before running the SQL below.

create table public.items (
  id uuid primary key default gen_random_uuid(),
  title text not null,
  description text,
  category text,
  image_url text,
  price numeric(10,2),
  slug text unique not null,
  created_at timestamptz not null default now()
);

create table public.item_embeddings (
  item_id uuid primary key references public.items(id) on delete cascade,
  embedding vector(1536) not null
);

create index on public.item_embeddings
  using hnsw (embedding vector_cosine_ops);

create table public.user_item_events (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade not null,
  item_id uuid references public.items(id) on delete cascade not null,
  event_type text not null check (event_type in ('view','click','add_to_cart','purchase')),
  created_at timestamptz not null default now()
);

alter table public.user_item_events enable row level security;

create policy "Users can insert own events"
  on public.user_item_events for insert
  with check (auth.uid() = user_id);

create policy "Users can read own events"
  on public.user_item_events for select
  using (auth.uid() = user_id);

create index user_item_events_user_idx
  on public.user_item_events (user_id, created_at desc);

create table public.recommendations_cache (
  user_id uuid references auth.users(id) on delete cascade not null,
  item_ids jsonb not null,
  generated_at timestamptz not null default now(),
  primary key (user_id)
);

alter table public.recommendations_cache enable row level security;

create policy "Users can read own cache"
  on public.recommendations_cache for select
  using (auth.uid() = user_id);

create policy "Service role manages cache"
  on public.recommendations_cache for all
  using (true)
  with check (true);
```

The HNSW index on item_embeddings is critical for query performance — without it, pgvector does a full sequential scan and times out above ~1,000 items. The recommendations_cache policy grants full access to the service role (used by Edge Functions) while restricting direct reads to the owning user.

## Build paths

### Lovable — fit 4/10, 4-6 hours

Lovable auto-generates the Supabase schema, Edge Function, and React card grid in one flow. The pgvector extension must be toggled on manually in Supabase Dashboard before the embedding queries will work.

1. Create a new Lovable project with Lovable Cloud enabled so Supabase auth and database are provisioned automatically
2. Go to Lovable Cloud tab → Database → open the Supabase Dashboard → Extensions → search 'vector' → enable pgvector
3. Paste the prompt below into Agent Mode and let it build the recommendation widget, event tracker, and Edge Function
4. Add your OpenAI API key in Cloud tab → Secrets as OPENAI_API_KEY so the Edge Function can call text-embedding-3-small
5. Publish the app and test recommendations on two different logged-in accounts to verify RLS is isolating event histories correctly

Starter prompt:

```
Build a product recommendations feature for a web app with an existing 'items' table (id, title, description, category, image_url, price, slug). Create these components: 1) A RecommendationsWidget React component that accepts currentItemId and userId props, calls a Supabase Edge Function 'get-recommendations', and renders up to 6 shadcn/ui Cards in a responsive CSS grid (2-col mobile, 3-col desktop) with skeleton loading states during fetch. Each card shows image, title, price, and an 'Add to cart' button. Show a 'Why recommended: Popular in this category' label under each card. Show a 'Trending items' fallback when the user has fewer than 3 events. 2) A Supabase Edge Function 'get-recommendations' that: reads the last 20 user events from user_item_events for the given userId; if fewer than 3 events exist, returns the 6 most-viewed items from the last 7 days (trending fallback); otherwise calls OpenAI text-embedding-3-small to embed the currentItem description and queries item_embeddings via pgvector cosine similarity for the 6 nearest neighbours, excluding already-viewed items and the currentItem itself; caches the result in recommendations_cache for 60 minutes. 3) An event tracker hook useTrackEvent(userId) that fires a Supabase insert into user_item_events for view, click, add_to_cart, and purchase events; debounce view events with a 2-second IntersectionObserver delay and a hasTracked ref to prevent duplicate inserts per item per session. Handle loading, error, and empty states. The feature should work for both authenticated and anonymous users — anonymous users always see trending items.
```

Limitations:

- pgvector extension must be enabled manually in Supabase Dashboard before embedding queries work — Lovable cannot do this automatically
- Cold-start users with fewer than 3 events always see trending fallback rather than personalised results; true personalisation needs data
- OpenAI embedding API calls add 200-400ms to Edge Function latency on first load; recommendations_cache reduces this to near zero for returning users

### V0 — fit 4/10, 5-8 hours

V0 excels at the recommendation card UI and API route shell. Vercel AI SDK handles streaming cleanly. Requires manual Supabase and pgvector setup outside V0.

1. Prompt V0 with the spec below to generate the recommendation card component and Next.js API route
2. In V0's Vars panel, add NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, and OPENAI_API_KEY
3. Run the SQL schema from this page in the Supabase SQL editor to create the required tables and HNSW index
4. Enable the pgvector extension in Supabase Dashboard → Database → Extensions before testing
5. Publish to Vercel and test recommendations with two separate authenticated users to verify RLS isolation

Starter prompt:

```
Build a product recommendations feature in Next.js App Router. Create: 1) A RecommendationsWidget client component that fetches from /api/recommendations?userId=X&itemId=Y, shows a 2-3 column responsive grid of shadcn/ui Cards (image, title, price, 'Add to cart' button, 'Popular in this category' label), uses shadcn/ui Skeleton for loading state, and shows a 'You might also like' heading. Handle empty state with 'Trending picks' fallback copy. 2) A Next.js Route Handler at app/api/recommendations/route.ts that: creates a Supabase server client with the service role key; checks user event count in user_item_events; if fewer than 3 events returns 6 most-viewed items from the last 7 days; otherwise calls OpenAI text-embedding-3-small to generate an embedding for the current item's title+description, queries item_embeddings via pgvector (<-> operator for cosine distance), excludes the current item and previously purchased items, returns up to 6 results; caches results in recommendations_cache for 1 hour. 3) A useTrackEvent React hook that debounces view events using IntersectionObserver with a 2-second threshold and a hasTracked ref; fires to Supabase user_item_events with event_type in ('view','click','add_to_cart','purchase'). Include TypeScript types for all components.
```

Limitations:

- Supabase and pgvector must be set up manually — V0 does not auto-provision databases
- Tailwind v3/v4 conflicts may cause shadcn/ui Card styling mismatches; follow up with 'fix shadcn imports' prompt if components look unstyled
- V0 sandbox preview cannot test authenticated Supabase queries — deploy to Vercel to verify RLS and event tracking

### Custom — fit 5/10, 1-2 weeks

Full custom development is the right path at 10K+ users where basic similarity breaks down, or when recommendation accuracy is a core product differentiator requiring ML model training and A/B testing.

1. Implement gradient boosting or neural collaborative filtering (LightFM, PyTorch) trained on full event history with explicit purchase signals weighted higher
2. Run simultaneous A/B tests on different recommendation algorithms with statistical significance tracking via PostHog or Statsig
3. Migrate from pgvector to a dedicated vector database (Pinecone, Weaviate) when catalogue exceeds 100K items to maintain sub-100ms query latency
4. Build a multi-channel recommendation model that unifies web, email, and push signals into one ranking

Limitations:

- Requires ML engineering expertise that is overkill for most apps under 50K monthly active users
- Training and serving ML models adds significant infrastructure cost and complexity

## Gotchas

- **All users see identical recommendations** — When the RLS policy is missing from user_item_events, every user's events merge into a shared pool. The recommendation engine sees the combined history of all users as if it belonged to the current user, so everyone gets the same globally popular items regardless of their personal behaviour. Fix: Add `USING (auth.uid() = user_id)` and `WITH CHECK (auth.uid() = user_id)` policies to user_item_events. Test by logging in as two different users, generating distinct event histories, and verifying the recommendations differ.
- **pgvector queries time out after ~500ms** — Without an index, pgvector performs a sequential scan of the entire item_embeddings table for every nearest-neighbour query. At 1,000+ items this regularly exceeds 500ms; at 5,000+ items it times out Edge Function execution entirely. Fix: Run `CREATE INDEX ON item_embeddings USING hnsw (embedding vector_cosine_ops);` in the Supabase SQL editor. HNSW indexes reduce query time to single-digit milliseconds even at 100K vectors. This must be created after populating the table with data.
- **New users always see the same 6 items (cold start)** — AI tools generate recommendation logic that assumes event history exists. When a new user with 0-2 events hits the recommendations widget, the pgvector query returns 0 results or falls back incorrectly, leaving the widget empty or throwing an unhandled error. Fix: Add an explicit conditional in the Edge Function: if `event_count < 3`, skip pgvector and instead return the 6 most-viewed items from the last 7 days ordered by view count. Your prompt must state this cold-start behaviour or AI tools will omit it.
- **Duplicate view events fire on every scroll** — When the event tracker is implemented with a basic React useEffect tied to element visibility, it re-fires every time the component re-renders — which on a scrollable product page can be dozens of times per session. This floods user_item_events with duplicate view rows that corrupt the collaborative filtering signal. Fix: Implement the impression tracker with IntersectionObserver and a `hasTracked` ref (a React ref object, not state) that is set to true on first fire and prevents all subsequent calls in the same session. Do not use state for this — state changes trigger re-renders which re-trigger the observer.

## Best practices

- Always implement cold-start fallback — show trending or bestseller items for users with fewer than 3 events rather than an empty widget
- Cache recommendation results in recommendations_cache with a 1-hour TTL; the pgvector query is expensive and results change slowly
- Track recommendation clicks separately from regular product clicks — you need position-in-list data to know which slots in the widget actually convert
- Debounce view events with IntersectionObserver and a hasTracked ref; never track a view on React re-render, only on first genuine scroll-into-view
- Exclude already-purchased items from recommendations — showing a product the user already bought is a trust signal failure
- Generate item embeddings asynchronously when items are created or updated, not synchronously in the product save API route
- Limit the recommendation widget to 6 items — more than 6 hurts click-through rate and makes the page feel algorithmic rather than curated
- Add a 'Why recommended' label (e.g. 'Popular in Kitchen') even if it is rule-based rather than ML-driven; it increases conversion by 15-25% in typical A/B tests

## Frequently asked questions

### How do product recommendations work when I have no user data yet?

Start with content-based filtering: embed your item descriptions with OpenAI and find the nearest neighbours to whatever product a visitor is viewing. This works for the first user on day one — no behavioural data needed. Collaborative filtering (based on what similar users bought) layers on top once you have 50-100 users with purchase history.

### Can I add recommendations to an existing Supabase database?

Yes. The only additions required are: enable the pgvector extension, add the item_embeddings table and HNSW index, add the user_item_events table with RLS, and deploy the Edge Function. Your existing items table does not need schema changes — the embedding table references it by item_id.

### What is the difference between collaborative filtering and content-based recommendations?

Content-based filtering looks at what an item is (title, description, category) and recommends similar items. Collaborative filtering looks at what similar users did (users who bought X also bought Y). Content-based works from day one with no user data; collaborative filtering gets more accurate as you collect purchase and view events. Most production systems blend both.

### How do I prevent recommendations from showing items the user already purchased?

In the Edge Function, query the user's purchase events first and collect the purchased item IDs, then add a NOT IN exclusion to the pgvector query. Your prompt should explicitly state 'exclude items the user has already purchased' — AI tools omit this logic unless instructed.

### Do I need a paid OpenAI account to build recommendations?

Yes — the Embeddings API has no free tier. However, costs are tiny: generating embeddings for a 1,000-item catalogue costs under $0.05 total with text-embedding-3-small. You only pay again when items are added or updated. The ongoing per-recommendation call only hits OpenAI when no cached result exists.

### How many user events are needed before recommendations become personalised?

Three to five events is a practical minimum — enough to establish a preference signal above random noise. Below that threshold, serve trending items as fallback. Genuine personalisation (recommendations that feel accurate to the user) typically requires 10-20 events per user including at least one purchase or add-to-cart.

### Can recommendations work for a small catalogue of 50 products?

Yes, and at 50 items you can skip pgvector entirely. Generate all pairwise embeddings once and store cosine similarity scores in a static JSON file or a simple cross-reference table. This approach has zero query latency and no pgvector complexity — practical until your catalogue exceeds 500-1,000 items.

### How do I track which recommendations actually converted to sales?

Fire a recommendation_click event when a user clicks a recommended card, storing the item_id, position in the widget, and the source_item_id (the product page they were on). Then join these clicks against purchase events to calculate your recommendation conversion rate. Build this tracking into the card's onClick handler before you go live — retrofitting it after is much harder.

---

Source: https://www.rapidevelopers.com/app-features/product-recommendations
© RapidDev — https://www.rapidevelopers.com/app-features/product-recommendations
