Feature spec
IntermediateCategory
ai-features
Build with AI
4-8 hours with Lovable or V0
Custom build
1-2 weeks custom dev
Running cost
$0/mo up to ~200 users; $25-30/mo at 1,000 users
Works on
Everything it takes to ship Product Recommendations — parts, prompts, and real costs.
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.
What users consider table stakes in 2026
- Recommendations appear without page reload — rendered inline as the product page loads, not after a visible delay
- Personalisation visibly improves after 2-3 interactions — a first-time user sees category trending, a returning user sees personalised picks
- A 'Why recommended' signal (e.g. 'Because you viewed X' or 'Popular in this category') increases trust and click-through rate
- Users can dismiss or hide individual recommendations; dismissed items stay hidden for the session
- Card grid is mobile-responsive — horizontal scroll on narrow viewports, 2- or 3-column grid on desktop
- Empty state handled gracefully when a new user has no events yet — show bestsellers or trending items, never a blank space
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
BackendSupabase 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.
Note: For catalogues under 5,000 items, SQL JOIN-based collaborative filtering (most co-viewed items) is simpler and faster than pgvector. Switch to vector similarity when catalogue grows or cross-category recommendations are needed.
User behaviour tracker
DataRecords 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.
Note: Debounce impression events — a scroll-past should not count as a view. Fire the insert only when an item is visible for at least 2 seconds using IntersectionObserver.
Recommendation API
BackendSupabase 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.
Note: Return the full item object (title, image_url, price, slug) from this function — avoid a second round-trip from the frontend.
Product card grid UI
UIReact 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.
Note: Skeleton cards prevent layout shift. Show exactly as many skeletons as the expected result count (6 is common) so the page doesn't jump.
Personalisation store
DataTwo 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.
Note: item_embeddings is not user-specific and can be read publicly; RLS only needed on user_item_events.
Analytics hook
BackendImpression 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.
Note: Debounce inserts and batch them — fire in groups of 5 or every 10 seconds to avoid hammering the database with single-row inserts.
The data model
Three tables cover the full recommendations data model. Run this in the Supabase SQL editor (Dashboard → SQL editor → New query):
1-- Enable pgvector extension (one-time, in Supabase Dashboard → Database → Extensions)2-- Search for "vector" and toggle on before running the SQL below.34create table public.items (5 id uuid primary key default gen_random_uuid(),6 title text not null,7 description text,8 category text,9 image_url text,10 price numeric(10,2),11 slug text unique not null,12 created_at timestamptz not null default now()13);1415create table public.item_embeddings (16 item_id uuid primary key references public.items(id) on delete cascade,17 embedding vector(1536) not null18);1920create index on public.item_embeddings21 using hnsw (embedding vector_cosine_ops);2223create table public.user_item_events (24 id uuid primary key default gen_random_uuid(),25 user_id uuid references auth.users(id) on delete cascade not null,26 item_id uuid references public.items(id) on delete cascade not null,27 event_type text not null check (event_type in ('view','click','add_to_cart','purchase')),28 created_at timestamptz not null default now()29);3031alter table public.user_item_events enable row level security;3233create policy "Users can insert own events"34 on public.user_item_events for insert35 with check (auth.uid() = user_id);3637create policy "Users can read own events"38 on public.user_item_events for select39 using (auth.uid() = user_id);4041create index user_item_events_user_idx42 on public.user_item_events (user_id, created_at desc);4344create table public.recommendations_cache (45 user_id uuid references auth.users(id) on delete cascade not null,46 item_ids jsonb not null,47 generated_at timestamptz not null default now(),48 primary key (user_id)49);5051alter table public.recommendations_cache enable row level security;5253create policy "Users can read own cache"54 on public.recommendations_cache for select55 using (auth.uid() = user_id);5657create policy "Service role manages cache"58 on public.recommendations_cache for all59 using (true)60 with check (true);Heads up: 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 it — pick your path
Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.
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.
Step by step
- 1Implement gradient boosting or neural collaborative filtering (LightFM, PyTorch) trained on full event history with explicit purchase signals weighted higher
- 2Run simultaneous A/B tests on different recommendation algorithms with statistical significance tracking via PostHog or Statsig
- 3Migrate from pgvector to a dedicated vector database (Pinecone, Weaviate) when catalogue exceeds 100K items to maintain sub-100ms query latency
- 4Build a multi-channel recommendation model that unifies web, email, and push signals into one ranking
Where this path bites
- 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
Third-party services you'll need
The core recommendation engine uses only Supabase and OpenAI. No third-party recommendation SaaS is required for an initial implementation:
| Service | What it does | Free tier | Paid from |
|---|---|---|---|
| OpenAI Embeddings API | Converts item titles and descriptions into 1536-dimension vectors for semantic similarity search via pgvector | No free tier; pay-as-you-go from first call | $0.02 per 1M tokens (approx, text-embedding-3-small, 2026) |
| Supabase Pro | PostgreSQL database with pgvector extension enabled; hosts all tables, Edge Functions, and auth | Free tier (2 projects, 500MB DB) — pgvector available but not guaranteed at scale | $25/mo (Pro, 8GB DB, pgvector fully supported) |
Swipe the table sideways to see pricing.
What it costs to run
Drag through the tiers to see how your monthly bill scales with users — no surprises later.
Estimated monthly running cost
Supabase free tier covers usage easily. Embedding generation for 100 items costs under $0.01. Recommendation queries negligible at this user count.
Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.
What breaks when AI tools build this
The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.
All users see identical recommendations
Symptom: 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
Symptom: 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)
Symptom: 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
Symptom: 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
When You Need Custom Development
Lovable and V0 comfortably cover personalised recommendations for catalogues under 50K items and user bases under 10K. Certain requirements push beyond what AI tools can deliver:
- Recommendation accuracy must improve with explicit ML model training — gradient boosting or neural collaborative filtering trained on full purchase history
- You need to A/B test different recommendation algorithms simultaneously and measure statistical significance of revenue lift
- Your catalogue exceeds 100K items — pgvector nearest-neighbour at this scale requires a dedicated vector database like Pinecone or Weaviate with sharding
- Multi-channel recommendations are needed — the same model must power web widgets, email campaigns, and push notifications from a single signal source
RapidDev builds these features for production
Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.
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.
Need this feature production-ready?
RapidDev builds product recommendations into real apps — auth, database, payments — at $13K–$25K.
Book a free consultation30-min call. No commitment.