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

- Tool: App Features
- Last updated: July 2026

## TL;DR

A smart recommendations feature needs four pieces: an event tracker (view/click/purchase/skip), a pgvector embedding store for items, a recommendation Edge Function that computes the user's interest centroid from recent events and retrieves similar items, and a cold-start fallback for new users. With Lovable or V0 you can ship a working recommender in 1–2 days. Running costs start near zero and scale to $25–60/mo at 1,000 users.

## What a Smart Recommendations Feature Actually Is

Smart recommendations surface items a specific user is likely to want next — products, articles, videos, or courses — based on their recent behavior rather than global popularity. The engine works in three steps: track what the user views, clicks, and skips; represent each content item as a 1536-dimension vector embedding (using OpenAI text-embedding-3-small); then find items whose embeddings are closest to the average of what the user has engaged with. The result is a personalized horizontal scroll strip or sidebar widget where each card shows a one-sentence AI-generated reason explaining why that item was surfaced. A cold-start fallback shows trending or popular items until the user has enough history for the personalized engine to kick in. The product decisions that make recommendations feel magical versus creepy are: showing the reason, making dismiss easy, and never mixing content types (product recommendations don't belong next to article suggestions).

## Anatomy of the Feature

Seven components. Event tracking and the popularity fallback are straightforward. The mean embedding centroid computation, reason caching, and skip exclusion are where first builds fail or burn unnecessary API credits.

- **Event Tracker** (data): A Supabase user_events table records every meaningful user interaction: view, click, purchase, like, and skip. Events fire from the client on user action — a view event fires when an item is visible in the viewport for more than 2 seconds, a skip event fires when the user dismisses a recommendation card. RLS restricts event reads to the owning user; service_role inserts are used for server-side event recording.
- **Embedding Store** (data): A Supabase items_embeddings table stores a 1536-dimension vector for each content item, generated by OpenAI text-embedding-3-small from the item's title, description, and tags concatenated. An ivfflat index enables sub-100ms cosine similarity search even at 100K items. Embeddings are generated when items are created or updated — not at query time.
- **Recommendation Engine** (backend): A Supabase Edge Function that implements the core logic: (1) fetch the user's last 20 events of type 'view' or 'click', (2) retrieve the embedding for each associated item from items_embeddings, (3) compute the mean embedding (the centroid — average across all 1536 dimensions), (4) run a pgvector cosine similarity search against items_embeddings excluding already-seen and skipped items, (5) return top-10 results with similarity scores.
- **Popularity Fallback** (backend): When a user has fewer than 5 recorded events (new user or cold start), the recommendation Edge Function queries the items table ordered by view_count DESC LIMIT 10 and returns popular items instead of personalized ones. The widget renders identically — no visual indicator that it's a fallback — preventing the jarring empty state on first visit.
- **Reason Generator** (backend): A call to claude-haiku-4-5 inside the recommendation Edge Function generates a one-sentence explanation: given the title of the item the user viewed and the title of the recommended item, return a reason string like 'Because you read about bootstrapping funding rounds'. Results are cached in recommendation_reasons by (item_a_id, item_b_id) primary key so each unique pair is generated only once.
- **Recommendations API** (backend): A Supabase Edge Function endpoint GET /recommendations?user_id=&item_type=&limit=10 that orchestrates the full pipeline: call Recommendation Engine, fetch item metadata (title, image, URL) for returned IDs, attach cached or freshly generated reasons, and return a JSON array. Responses are cached at the CDN layer (Cache-Control: max-age=300) so repeat page loads within 5 minutes don't re-run the vector search.
- **Recommendation Widget** (ui): A horizontal scroll strip (mobile) or sidebar/grid section (desktop) rendered below the fold. Each card shows the item thumbnail, title, and AI reason chip. An X dismiss button fires a skip event via the Recommendations API and removes the card with a smooth CSS transition. The widget lazy-loads using IntersectionObserver so it doesn't block above-the-fold page speed. Each card has a descriptive aria-label combining item title and reason for accessibility.

## Data model

Three tables: user_events (behavioral signals), items_embeddings (vector representations of content), and recommendation_reasons (AI reason cache). The pgvector extension and ivfflat index are required. Run this in the Supabase SQL Editor.

```sql
-- Enable pgvector extension (once per project)
create extension if not exists vector;

-- User behavior events
create table public.user_events (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade not null,
  event_type text not null check (event_type in ('view', 'click', 'purchase', 'like', 'skip')),
  item_id uuid not null,
  item_type text not null,
  metadata jsonb,
  session_id uuid,
  created_at timestamptz not null default now()
);

create index user_events_user_type_idx
  on public.user_events (user_id, event_type, created_at desc);

-- Item vector embeddings (pre-indexed)
create table public.items_embeddings (
  id uuid primary key default gen_random_uuid(),
  item_id uuid not null,
  item_type text not null,
  embedding vector(1536) not null,
  updated_at timestamptz not null default now()
);

create unique index items_embeddings_item_type_idx
  on public.items_embeddings (item_id, item_type);

-- ivfflat index for fast cosine similarity search
create index items_embeddings_vector_idx
  on public.items_embeddings
  using ivfflat (embedding vector_cosine_ops)
  with (lists = 100);

-- AI reason cache: one row per unique item pair
create table public.recommendation_reasons (
  item_a_id uuid not null,
  item_b_id uuid not null,
  reason text not null,
  created_at timestamptz not null default now(),
  primary key (item_a_id, item_b_id)
);

-- RLS policies
alter table public.user_events enable row level security;
alter table public.items_embeddings enable row level security;
alter table public.recommendation_reasons enable row level security;

create policy "Service role can insert events"
  on public.user_events for insert
  to service_role
  with check (true);

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

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

create policy "Authenticated users can read embeddings"
  on public.items_embeddings for select
  to authenticated
  using (true);

create policy "Authenticated users can read reasons"
  on public.recommendation_reasons for select
  to authenticated
  using (true);

create policy "Service role can insert reasons"
  on public.recommendation_reasons for insert
  to service_role
  with check (true);
```

The ivfflat index is critical for performance — without it pgvector does a full sequential scan that becomes slow past 10K items. The unique index on items_embeddings(item_id, item_type) prevents duplicate embeddings for the same item. The composite index on user_events makes the 'last 20 viewed items' query that drives centroid computation fast even for power users with thousands of events.

## Build paths

### Lovable — fit 3/10, 1–2 days

Lovable can wire event tracking, the pgvector similarity search, cold-start fallback, and the recommendation widget. The multi-Edge-Function architecture requires careful, explicit prompting — expect 2–4 follow-up iterations. Best path if the app is already in Lovable.

1. Open Cloud tab → Database → SQL Editor and run the full schema above (pgvector extension, all three tables, RLS, ivfflat index)
2. Open Cloud tab → Secrets and add OPENAI_API_KEY and ANTHROPIC_API_KEY
3. Run a one-time content indexing Edge Function (prompt Lovable to create it) that embeds all existing items and populates items_embeddings
4. Paste the prompt below into Agent Mode; expect at least one follow-up prompt to fix the centroid computation if Lovable misses it
5. After the build, verify in the Supabase Dashboard Table Editor that user_events rows are being inserted when you interact with items on the published URL

Starter prompt:

```
Build a smart recommendations feature for this app. Event tracking: fire a Supabase INSERT into user_events table on every item view (when card is visible >2s via IntersectionObserver), click, and skip. Fields: user_id (from auth), event_type, item_id, item_type ('product' | 'article'), session_id. Recommendation Edge Function GET /recommendations: (1) Query user_events for this user's last 20 rows WHERE event_type IN ('view', 'click'); (2) For each event, fetch the item's vector from items_embeddings; (3) Compute the MEAN of all 1536-dimension vectors element-by-element — this is the user interest centroid; (4) pgvector cosine similarity search: SELECT item_id FROM items_embeddings WHERE item_type = $item_type AND embedding <=> $centroid < 0.3 AND item_id NOT IN (SELECT item_id FROM user_events WHERE user_id = $uid AND event_type = 'skip') ORDER BY embedding <=> $centroid LIMIT 10; (5) Cold start: if user has <5 events, query items table ORDER BY view_count DESC LIMIT 10 instead. Reason generation: for the top-viewed item in the user's history and each recommended item, check recommendation_reasons table for cached reason; if missing, call claude-haiku-4-5: 'In one short sentence starting with Because, explain why someone who read [source title] would like [recommended title]. Return only the sentence.' Insert result into recommendation_reasons. Return: JSON array with item_id, title, image_url, reason, similarity_score. Widget: horizontal scroll strip rendered below the fold using IntersectionObserver lazy loading. Each card: thumbnail, title, reason chip, X dismiss button that fires skip event and removes card with CSS fade transition. Each card aria-label must include item title and reason. Loading skeleton while Edge Function runs. Empty state only if cold start also returns nothing.
```

Limitations:

- Lovable's 70% problem is most likely to hit on the mean embedding centroid computation — if recommendations seem random, check whether the Edge Function is actually computing the vector mean or just using the most recent item's embedding
- pgvector ivfflat index creation may be missed — verify it exists in Supabase Dashboard → Database → Indexes after the build
- Content pre-indexing (populating items_embeddings) must be done separately before recommendations can work; Lovable won't create this pipeline without explicit prompting

### V0 — fit 4/10, 1–2 days

V0 generates a clean Next.js recommendation widget with React Server Components for fast first load and API routes for the pgvector query and reason generation. Best path when recommendations will be integrated into a Next.js app with existing product or content data.

1. Prompt V0 with the spec below; it generates the RecommendationStrip client component and the /api/recommendations route
2. Add OPENAI_API_KEY and ANTHROPIC_API_KEY in the Vars panel
3. Run the schema SQL in Supabase (or Neon on Vercel) to create all tables and the ivfflat index
4. Run a one-time batch indexing script to populate items_embeddings for existing content
5. Deploy to Vercel and verify event tracking fires on the production URL by checking the Supabase Table Editor for new user_events rows

Starter prompt:

```
Create a Next.js smart recommendations feature. API Route: /api/recommendations (GET, accepts user_id, item_type, limit=10). Logic: (1) Supabase query: SELECT e.item_id, ie.embedding FROM user_events e JOIN items_embeddings ie ON ie.item_id = e.item_id WHERE e.user_id = $uid AND e.event_type IN ('view','click') ORDER BY e.created_at DESC LIMIT 20; (2) If fewer than 5 results, return items ORDER BY view_count DESC LIMIT 10 (cold start path); (3) Compute mean embedding: for each of the 1536 dimensions, average the values across all fetched vectors — this is the interest centroid; (4) pgvector RPC call to Supabase: SELECT item_id, 1 - (embedding <=> $centroid) AS score FROM items_embeddings WHERE item_type = $item_type AND item_id NOT IN (SELECT item_id FROM user_events WHERE user_id = $uid AND event_type = 'skip') ORDER BY embedding <=> $centroid LIMIT 10; (5) For each result, check recommendation_reasons for cached reason; if not found, call claude-haiku-4-5 via Anthropic SDK to generate 'Because...' sentence, insert into cache; (6) Return JSON array: {item_id, title, image_url, url, reason, score}. Client Component: RecommendationStrip — horizontal scrollable flex row; each RecommendationCard shows image, title, reason badge, X button; X fires POST /api/events with event_type: 'skip' and removes card with CSS opacity transition. useIntersectionObserver for lazy loading — fetch only when strip enters viewport. Loading skeleton (3 cards with pulse animation). Event tracking: useEffect fires POST /api/events with event_type: 'view' after 2s visibility on each card. Fully accessible: aria-label on each card = title + reason. Export: default RecommendationStrip and named RecommendationCard for embedding anywhere.
```

Limitations:

- V0 does not auto-provision Supabase or the content embedding pipeline — both must be set up and populated manually before recommendations return meaningful results
- The cold start fallback (popularity-based) requires a view_count column on your items table maintained by a trigger — V0 may not generate this automatically
- Recommendation reasons are generated fresh on first page load until the cache warms up — brief latency on first visits is expected

### Custom — fit 5/10, 3–6 weeks

Full collaborative filtering with user-user and item-item similarity, A/B testing for algorithm variants, real-time event streaming via Supabase Realtime, recommendation quality metrics dashboard, and scheduled re-indexing jobs for updated content.

1. Real-time event streaming: Supabase Realtime broadcasts user events to a server process that updates a user_interest_embeddings materialized view asynchronously, eliminating centroid recomputation on every recommendation request
2. A/B testing framework: randomly assign users to algorithm variants (pgvector centroid vs collaborative filtering vs popularity) and track CTR and conversion lift per variant
3. Recommendation quality dashboard: CTR by recommendation position, skip rate, conversion from recommendation to purchase, and coverage (percentage of catalog appearing in at least one recommendation)
4. Scheduled re-indexing: a daily Supabase Edge Function cron job re-embeds items whose descriptions have changed and updates items_embeddings accordingly
5. Optional: migrate from pgvector to a dedicated vector database (Pinecone or Qdrant) when the items catalog exceeds 500K rows

Limitations:

- True collaborative filtering (user-user similarity) at scale requires ML infrastructure beyond pgvector and is typically better served by managed services like Google Recommendations AI or AWS Personalize
- Building statistically significant A/B tests requires sustained traffic and adds 1–2 weeks to development timeline

## Gotchas

- **Recommendations show same items to every user** — Either user_events are not being saved (the RLS INSERT policy blocks client-side inserts, so every user appears to have zero events and falls through to the popularity fallback) or the centroid computation falls back silently to a default embedding vector when the Edge Function errors. The result looks like a popularity widget rather than personalization. Fix: In Supabase Dashboard → Table Editor, open user_events and check that rows appear after interacting with items on the live site. If the table stays empty, the RLS INSERT policy is blocking — verify the policy allows authenticated users to insert rows where auth.uid() = user_id. Check Edge Function logs in the Supabase Dashboard for centroid computation errors.
- **New users see a completely empty recommendations section** — The Edge Function returns an empty array when the user has no events, and the frontend renders nothing — a blank space below the fold that makes the page look broken. This is one of the most common first-build failures because the cold start path is often omitted entirely on the first AI generation pass. Fix: Add an explicit branch in the Edge Function: if the user_events query returns fewer than 5 rows, query the items table ORDER BY view_count DESC LIMIT 10 and return those instead. The widget should always render at least 10 items on first visit. Add this check explicitly in the prompt — AI tools almost always omit it without being asked.
- **Recommendation reasons burn Claude credits on every page load** — The recommendation_reasons cache table is checked but the cache key doesn't match — perhaps because item_a_id and item_b_id are passed in inconsistent order, or because the cache INSERT fails due to an RLS policy. Every page load calls Claude Haiku for every displayed recommendation, turning a $0.001/pair one-time cost into a per-page-load cost that can add up fast. Fix: Normalize the cache key order: always store and look up with the smaller UUID as item_a_id. Check the recommendation_reasons RLS policy — service_role must be able to INSERT. Add logging in the Edge Function to confirm cache hits before shipping to production. Verify the cache is populating by checking the table in Supabase Dashboard after 10–20 page loads.
- **Dismissed items reappear on the next page load** — The skip event fires correctly and the card disappears from the current session's UI via state update. But on next page load, the recommendation Edge Function doesn't exclude skipped items from the pgvector query — the dismissed items reappear at the top of every recommendations section. Users notice this immediately and lose trust in the feature. Fix: In the Edge Function, add a NOT IN subquery to the pgvector similarity search: AND item_id NOT IN (SELECT item_id FROM user_events WHERE user_id = $uid AND event_type = 'skip'). This must be part of the prompt explicitly — AI tools generate the similarity query without the skip exclusion by default.
- **Recommendations are slow (2+ seconds) for active users** — The centroid computation fetches the user's last 20 event rows and then fetches each associated embedding — 20 separate database reads — then averages the vectors in application code. This sequential read pattern scales poorly and adds 1–2 seconds for users with significant history. At the same time, the ivfflat index may have been missed, forcing a sequential scan of the entire embeddings table. Fix: Two fixes: (1) Create a user_interest_embeddings table with one row per user storing the precomputed centroid, updated asynchronously via a Supabase database trigger after each new event. (2) Verify the ivfflat index exists in Supabase Dashboard → Database → Indexes. Both fixes together typically bring recommendation latency below 200ms.

## Best practices

- Always include the cold start fallback in the initial build — a blank recommendations section on first visit is one of the most damaging first impressions a feature can make
- Cache recommendation reasons in recommendation_reasons by (item_a_id, item_b_id) pair immediately after generation — the cache warms up within hours and Claude costs drop to near zero for returning users
- Exclude skipped items from the pgvector query with a NOT IN subquery — users who dismiss a recommendation expect it to never return
- Precompute and cache the user interest centroid in a separate table updated asynchronously after each event rather than recomputing it on every recommendation request
- Filter by item_type in the pgvector query so product recommendations never mix with article suggestions — type-mixed recommendations feel incoherent and reduce click-through
- Add an aria-label to each recommendation card that includes both the item title and the reason chip text — screen reader users experience recommendations through these labels alone
- Lazy-load the recommendation widget using IntersectionObserver so it doesn't block above-the-fold Core Web Vitals scores
- Track skip rate per recommendation position and per item — a skip rate above 40% on position 1 usually means the centroid computation has a bug or the embedding model mismatch issue is present

## Frequently asked questions

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

Collaborative filtering recommends items that users similar to you liked ('users who bought this also bought...'). It requires enough user behavior data across many users to find meaningful similarity clusters — it struggles with cold start and sparse data. Content-based recommendations (what this build kit uses) embed each item as a vector and find items semantically similar to what the current user has engaged with. Content-based works with a single user's history and is easier to build with pgvector.

### How do I handle new users with no history?

Use a popularity fallback: when a user has fewer than 5 recorded events, return items ordered by view_count DESC. This requires maintaining a view_count column on your items table, incremented by a Supabase database trigger each time a view event is recorded. After the user accumulates 5+ interactions, the personalized centroid engine takes over automatically. Never show an empty recommendations section.

### How accurate is pgvector for recommendations?

For content-based recommendations, pgvector cosine similarity with text-embedding-3-small embeddings produces surprisingly good results for text-heavy content like articles, blog posts, and product descriptions. Quality drops for content that relies heavily on images (where you'd need image embeddings from CLIP or similar) or for highly sparse catalogs with fewer than 500 items. For comparison, Google Recommendations AI uses collaborative filtering and behavioral signals that pgvector doesn't replicate — but it costs significantly more.

### How often should I re-index content embeddings?

Re-index whenever item content changes — if a product description is updated or an article is edited, its embedding is stale and may surface it in irrelevant contexts. The best approach is a Supabase database trigger on the items table that fires an Edge Function to re-embed the changed item immediately. For large catalogs (10K+ items) where batch re-indexing would be expensive, consider re-indexing only items modified in the last 7 days on a daily schedule.

### How do I track which recommendations convert to purchases?

Record a 'click' event when a user taps a recommendation card, and a 'purchase' event when they complete a purchase. JOIN user_events on session_id to see whether a purchase in the same session was preceded by a recommendation click for that item_id. The conversion rate from recommendation click to purchase (recommendation-assisted conversion) is your primary quality metric. Track this per item_type and over time to measure whether recommendation quality is improving.

### Can I show different recommendations on different pages?

Yes — the Recommendations API accepts an item_type parameter that filters the pgvector query to a specific content category. On a product page, pass item_type='product' to show only product recommendations. On a blog page, pass item_type='article'. You can also pass a context_item_id to bias recommendations toward items similar to the one currently being viewed, rather than the user's full history centroid.

### How do I prevent filter bubbles where users only see more of the same?

Three approaches: (1) Mix in a diversity factor — after the top-10 pgvector results, randomly replace 2 cards with items from categories the user hasn't engaged with yet. (2) Add a 'freshness boost' to the similarity score for items created in the last 7 days. (3) Implement a skip-rate threshold: if a category has a skip rate above 50%, stop recommending from it for that user for 30 days.

### When should I use Google Recommendations AI instead of pgvector?

pgvector centroid-based recommendations work well up to ~100K items and ~10K users where you have sufficient event data. Consider Google Recommendations AI when you have more than 1M catalog items, need collaborative filtering (not just content similarity), require managed retraining, or need built-in A/B testing infrastructure. The trade-off: Google Recommendations AI costs $0.27 per 1K predictions and requires significant setup versus the pgvector approach that runs within your existing Supabase bill.

---

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