Feature spec
AdvancedCategory
ai-features
Build with AI
1–2 days with Lovable
Custom build
3–6 weeks custom dev
Running cost
$0–25/mo up to 100 users; $25–60/mo at 1K users
Works on
Everything it takes to ship Smart Recommendations — parts, prompts, and real costs.
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).
What users consider table stakes in 2026
- Recommendations appear inline on the current page — in a sidebar, below-the-fold strip, or modal — never requiring the user to navigate to a separate 'For You' page
- Each recommendation card shows a concise AI-generated reason chip explaining why it was suggested ('Because you viewed X')
- One-click dismiss button on each card that removes the item immediately and records a skip event so it never reappears
- New users see a 'Popular this week' fallback section — the recommendations area is never blank on first visit
- Recommendations refresh automatically on each session without a manual reload or re-login
- Widget is accessible as a horizontal scroll strip on mobile and a sidebar or grid section on desktop
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
DataA 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.
Note: Fire events optimistically from the client without waiting for confirmation to keep the UI fast. The event volume per user is low (20–100 events/month typical) so Supabase insert costs are negligible.
Embedding Store
DataA 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.
Note: Pre-indexing is a one-time setup cost: embedding 10K items costs about $0.20 and takes a few minutes via a batch Edge Function. Add a Supabase database trigger to embed new items automatically on INSERT to the items table.
Recommendation Engine
BackendA 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.
Note: The centroid computation is the part AI tools most often get wrong — they average only the item IDs or omit the step entirely. The prompt must explicitly specify 'compute the mean of the 1536-dimension vectors' and show the math.
Popularity Fallback
BackendWhen 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.
Note: Keep a view_count column on your items table incremented via a Supabase database trigger on INSERT into user_events WHERE event_type = 'view'. This avoids a COUNT query on every recommendation request.
Reason Generator
BackendA 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.
Note: Check the cache before calling Claude. Each new reason costs approximately $0.001; after warm-up the cache hit rate is typically above 80%, keeping ongoing Claude costs near zero for returning users.
Recommendations API
BackendA 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
UIA 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.
The 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.
1-- Enable pgvector extension (once per project)2create extension if not exists vector;34-- User behavior events5create table public.user_events (6 id uuid primary key default gen_random_uuid(),7 user_id uuid references auth.users(id) on delete cascade not null,8 event_type text not null check (event_type in ('view', 'click', 'purchase', 'like', 'skip')),9 item_id uuid not null,10 item_type text not null,11 metadata jsonb,12 session_id uuid,13 created_at timestamptz not null default now()14);1516create index user_events_user_type_idx17 on public.user_events (user_id, event_type, created_at desc);1819-- Item vector embeddings (pre-indexed)20create table public.items_embeddings (21 id uuid primary key default gen_random_uuid(),22 item_id uuid not null,23 item_type text not null,24 embedding vector(1536) not null,25 updated_at timestamptz not null default now()26);2728create unique index items_embeddings_item_type_idx29 on public.items_embeddings (item_id, item_type);3031-- ivfflat index for fast cosine similarity search32create index items_embeddings_vector_idx33 on public.items_embeddings34 using ivfflat (embedding vector_cosine_ops)35 with (lists = 100);3637-- AI reason cache: one row per unique item pair38create table public.recommendation_reasons (39 item_a_id uuid not null,40 item_b_id uuid not null,41 reason text not null,42 created_at timestamptz not null default now(),43 primary key (item_a_id, item_b_id)44);4546-- RLS policies47alter table public.user_events enable row level security;48alter table public.items_embeddings enable row level security;49alter table public.recommendation_reasons enable row level security;5051create policy "Service role can insert events"52 on public.user_events for insert53 to service_role54 with check (true);5556create policy "Users can insert own events"57 on public.user_events for insert58 to authenticated59 with check (auth.uid() = user_id);6061create policy "Users can read own events"62 on public.user_events for select63 using (auth.uid() = user_id);6465create policy "Authenticated users can read embeddings"66 on public.items_embeddings for select67 to authenticated68 using (true);6970create policy "Authenticated users can read reasons"71 on public.recommendation_reasons for select72 to authenticated73 using (true);7475create policy "Service role can insert reasons"76 on public.recommendation_reasons for insert77 to service_role78 with check (true);Heads up: 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 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 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.
Step by step
- 1Real-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
- 2A/B testing framework: randomly assign users to algorithm variants (pgvector centroid vs collaborative filtering vs popularity) and track CTR and conversion lift per variant
- 3Recommendation quality dashboard: CTR by recommendation position, skip rate, conversion from recommendation to purchase, and coverage (percentage of catalog appearing in at least one recommendation)
- 4Scheduled re-indexing: a daily Supabase Edge Function cron job re-embeds items whose descriptions have changed and updates items_embeddings accordingly
- 5Optional: migrate from pgvector to a dedicated vector database (Pinecone or Qdrant) when the items catalog exceeds 500K rows
Where this path bites
- 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
Third-party services you'll need
The recommendation engine itself is cheap to run. OpenAI embeddings are priced per token at very low rates; Claude Haiku reason generation is nearly free after the cache warms up. The main ongoing cost is Supabase compute for the pgvector queries.
| Service | What it does | Free tier | Paid from |
|---|---|---|---|
| OpenAI Embeddings API (text-embedding-3-small) | Generates 1536-dimension vectors for content items (one-time at index) and for user interest centroids (at query time) | No free tier; pay-as-you-go | $0.02 per 1M tokens; indexing 10K items ≈ $0.20 one-time; recommendation queries ≈ $0.001 per session |
| Anthropic Claude (claude-haiku-4-5) | Generates one-sentence recommendation reasons per item pair; cached in recommendation_reasons table after first generation | No free tier; pay-as-you-go | ~$0.001 per unique item pair reason; very low after cache warms up |
| Supabase pgvector | Vector similarity search for finding items close to the user's interest centroid; included in all Supabase plans | Available on Free plan (limited compute) | Pro $25/mo includes sufficient compute for recommendation queries at up to ~1M items |
| Google Recommendations AI (alternative) | Managed ML recommendation service; trained model handles collaborative filtering without pgvector setup | No free tier | $0.27 per 1K prediction requests (approx); meaningful cost at scale |
| AWS Personalize (alternative) | Managed recommendation service using behavioral data; handles cold start automatically | No free tier after 2-month trial | Training $0.24/hr + inference $0.20 per 1K recommendations (approx) |
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 the database. OpenAI embedding queries for 100 users × 5 sessions/mo = 500 recommendation requests at $0.001 each = $0.50. Claude reasons generate once per item pair and cache — negligible after first day. Total dominated by any Supabase plan cost; stays free on the free tier.
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.
Recommendations show same items to every user
Symptom: 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
Symptom: 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
Symptom: 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
Symptom: 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
Symptom: 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
When You Need Custom Development
pgvector centroid-based recommendations work well for most apps. Several scenarios push beyond what this architecture can reliably handle:
- Requires A/B testing multiple recommendation algorithms (collaborative filtering vs content-based vs hybrid) with statistical significance measurement and automated winner promotion
- Multi-armed bandit optimization to balance exploration of new items versus exploitation of known user preferences — this goes beyond mean centroid into active learning territory
- Real-time recommendations that update within seconds of a user interaction, requiring a streaming event pipeline rather than the batch centroid update approach
- Recommendation quality reporting for business stakeholders — CTR lift, conversion lift, coverage metrics, and diversity scores — requires a dedicated analytics layer and dashboard beyond what Supabase Table Editor provides
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
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.
Need this feature production-ready?
RapidDev builds smart recommendations into real apps — auth, database, payments — at $13K–$25K.
Book a free consultation30-min call. No commitment.