Feature spec
IntermediateCategory
communication-social
Build with AI
6-10 hours with Lovable or V0
Custom build
1-2 weeks custom dev
Running cost
$0/mo up to 1,000 users · $25-75/mo at 10K users
Works on
Everything it takes to ship an User to User Recommendation System — parts, prompts, and real costs.
A user-to-user recommendation system needs four pieces: a recommend button with Fuse.js recipient search, a polymorphic recommendations table that works for any content type (products, articles, profiles), a Supabase Edge Function that triggers an in-app notification on each INSERT, and an inbox where recipients see what's been shared. Ship it with Lovable or V0 in 6-10 hours for $0/month on the free tier. AI tools frequently omit the self-recommendation guard — always check.
What a User-to-User Recommendation System Actually Requires
A recommendation system where users share items with each other is architecturally different from algorithmic recommendations. You need three surfaces: the recommend action (a button on any content item that opens a recipient picker), the notification delivery (a Supabase Edge Function triggered on INSERT that pushes a badge update to the recipient in real time via Supabase Realtime), and the recommendations inbox (a dedicated page or panel where users see what has been shared with them, mark items as read, and dismiss or save recommendations). The polymorphic data model — where recommendations reference any content type via item_type and item_id — is what makes the feature extensible across your whole app rather than locked to one section.
What users consider table stakes in 2026
- One-click 'Recommend to a friend' action from any content item — the action should be no more than 2 clicks from the item
- Recipient selection via a searchable combobox with fuzzy matching so users can find friends by partial name — not a dropdown of all users
- Optional personal note field so the recommendation carries context ('thought you'd love this because...')
- Recipient sees an in-app notification badge update in real time — not just on next page load
- Dedicated recommendations inbox with unread/read state, sender avatar, item preview, and note visible at a glance
- Ability to dismiss a recommendation or save it to a list — unread recommendations should not feel like an obligation
Anatomy of the Feature
Six components. The recommend button and inbox UI generate reliably. The polymorphic item_type pattern, Fuse.js recipient combobox debounce, and self-recommendation guard are where first builds fail.
Recommend action button
UIshadcn/ui Button (secondary, small) placed on any content card or detail page. On click, opens a Popover with: a combobox for recipient search (Fuse.js fuzzy matching over a preloaded list of followees or all users, 300ms debounce), an optional note textarea (max 280 characters), and a confirm 'Recommend' button. The send button is disabled when sender === recipient.
Note: Preload the users list once on component mount (limit to followees or friends list for apps with social graphs). Do not run a Supabase query on every keystroke — debounce Fuse.js client-side search over the preloaded array instead.
Recommendation record
DataSupabase recommendations table with polymorphic item reference: sender_id, recipient_id, item_type (text enum: 'product', 'article', 'user', 'post', etc.), item_id (UUID), note (text, max 280 chars), created_at, read_at. A database-level CHECK constraint prevents self-recommendation (sender_id != recipient_id). An item_snapshot_title column stores a copy of the item's display name at send time to prevent orphan recommendations if content is deleted.
Note: The polymorphic (item_type, item_id) pattern avoids a separate recommendations table per content type. Store item_snapshot_title at insert time — if the original content is deleted, the inbox still shows what was recommended.
In-app notification
BackendSupabase Database Webhook on recommendations table INSERT triggers a Supabase Edge Function. The Edge Function inserts a row into a notifications table (user_id = recipient_id, type = 'recommendation', payload JSONB containing sender name, item title, and note). Supabase Realtime Postgres Changes subscription on notifications table (filtered by user_id) updates the recipient's notification badge count in real time.
Note: Using a Database Webhook trigger instead of a direct Edge Function call decouples the recommendation INSERT from the notification delivery — if the notification fails, the recommendation is still saved. The payload JSONB should include all data needed to render the notification without an additional query.
Recommendations inbox
UIDedicated /recommendations route (or an inbox panel accessible from the nav badge). Lists received recommendations grouped by unread/read, newest first. Each row shows sender avatar, item preview card (title, thumbnail), note, and relative timestamp. Mark-as-read on row click (UPDATE read_at = now()). Dismiss button (soft-delete with dismissed_at column). Bulk mark-all-read action.
Note: Use an index on (recipient_id, created_at DESC) and a separate index on (recipient_id, read_at) for the unread count query. Without these, the inbox query scans the full recommendations table for large apps.
Recommendation analytics
DataAggregate queries over the recommendations table: most recommended items (SELECT item_id, item_type, COUNT(*) GROUP BY item_id, item_type ORDER BY count DESC), top recommenders, and conversion rate (percentage of recommendations where recipient subsequently viewed or purchased the item). Exposed via Supabase RPC functions or a Supabase materialized view refreshed daily.
Note: Materialized view is preferred over a live aggregate query for the 'most recommended' leaderboard — a real-time COUNT(*) GROUP BY over a large recommendations table is slow and unnecessary for a leaderboard that changes gradually.
Email digest (optional)
ServiceSupabase Edge Function on a weekly pg_cron schedule queries unread recommendations per user, generates an email summary using React Email templates, and sends via Resend API. Email includes sender names, item thumbnails, and a 'View in app' CTA link. Subject: 'You have N new recommendations from [friends]'.
Note: Only send the digest to users who have unread recommendations at send time. Batch users to avoid hitting Resend rate limits — process 100 users per Edge Function invocation with cursor pagination.
The data model
Two tables: recommendations (the core record) and notifications (the real-time delivery mechanism). Run this in the Supabase SQL Editor (Dashboard → SQL Editor):
1create table public.recommendations (2 id uuid primary key default gen_random_uuid(),3 sender_id uuid references auth.users(id) on delete cascade not null,4 recipient_id uuid references auth.users(id) on delete cascade not null,5 item_type text not null,6 item_id uuid not null,7 item_snapshot_title text,8 note text check (length(note) <= 280),9 created_at timestamptz not null default now(),10 read_at timestamptz,11 dismissed_at timestamptz,12 constraint no_self_recommendation check (sender_id != recipient_id)13);1415create table public.notifications (16 id uuid primary key default gen_random_uuid(),17 user_id uuid references auth.users(id) on delete cascade not null,18 type text not null,19 payload jsonb not null default '{}',20 created_at timestamptz not null default now(),21 read_at timestamptz22);2324alter table public.recommendations enable row level security;25alter table public.notifications enable row level security;2627create policy "Sender can insert recommendations"28 on public.recommendations for insert29 with check (auth.uid() = sender_id);3031create policy "Recipient can read their received recommendations"32 on public.recommendations for select33 using (auth.uid() = recipient_id);3435create policy "Sender can read their sent recommendations"36 on public.recommendations for select37 using (auth.uid() = sender_id);3839create policy "Recipient can update read_at and dismissed_at"40 on public.recommendations for update41 using (auth.uid() = recipient_id)42 with check (auth.uid() = recipient_id);4344create policy "Users can read their own notifications"45 on public.notifications for select46 using (auth.uid() = user_id);4748create policy "Users can update their own notifications read_at"49 on public.notifications for update50 using (auth.uid() = user_id);5152create index recommendations_recipient_idx53 on public.recommendations (recipient_id, created_at desc);5455create index recommendations_recipient_unread_idx56 on public.recommendations (recipient_id, read_at)57 where read_at is null;5859create index recommendations_sender_idx60 on public.recommendations (sender_id, created_at desc);6162create index notifications_user_id_idx63 on public.notifications (user_id, created_at desc);6465create index notifications_user_unread_idx66 on public.notifications (user_id, read_at)67 where read_at is null;Heads up: The no_self_recommendation CHECK constraint is enforced at the database level — even if the client-side button is disabled, a direct API call cannot insert a self-recommendation. The item_snapshot_title column preserves the display name at send time; if the original content is deleted, the recommendation inbox still shows a meaningful title.
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 control over recommendation ML scoring, collaborative filtering extensions, CRM export, reputation gamification, and complex privacy controls for private accounts.
Step by step
- 1Extend the recommendations table with a conversion_event_at column and wire it to your analytics events (view, purchase, save) to compute recommendation conversion rates per sender
- 2Add a reputation system: a materialized view aggregating recommendation_count, conversion_rate, and avg_recipient_engagement per user, updated nightly via pg_cron; display as a 'Top Recommender' badge on profile pages
- 3Implement collaborative filtering as an extension: when user A recommends item X to user B, also surface item X to other users in A and B's social graph using a Supabase RPC function weighted by recommendation count
- 4Add privacy controls: private accounts require the sender to be a confirmed follower before they can send a recommendation; multi-step approval flow where recipient must accept before seeing recommendation content
Where this path bites
- Collaborative filtering and ML-based recommendation scoring require data science infrastructure (Python, numpy, trained models) that takes 3-4 weeks beyond the core feature to build well
Third-party services you'll need
The recommendation system runs entirely on Supabase free tier for most apps. Costs only appear when notification Realtime load is high or when the weekly email digest volume exceeds Resend's free tier:
| Service | What it does | Free tier | Paid from |
|---|---|---|---|
| Supabase | Database (recommendations + notifications), Realtime for notification badge, Edge Functions for notification delivery, Database Webhooks trigger | Free — 500 concurrent connections, 500MB database, 500K Edge Function invocations/mo | Pro $25/mo — 10,000 concurrent connections, 8GB database |
| Resend | Weekly email digest of new recommendations with item previews and sender names | 3,000 emails/mo free | $20/mo (50,000 emails) (approx) |
| Fuse.js | Client-side fuzzy search over the users list for the recipient combobox — runs entirely in the browser with no API call per keystroke | Free (MIT license) | Free |
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 all recommendations, notifications, and Realtime connections. Resend free tier (3,000 emails/mo) covers weekly digests. Fuse.js is free. Total: $0/mo.
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.
Self-recommendation not blocked — users recommend items to themselves
Symptom: AI tools commonly omit the CHECK (sender_id != recipient_id) database constraint. The client-side button disables itself when sender matches recipient, but a direct Supabase insert (via API or a third-party tool) bypasses the UI guard and creates self-recommendation rows. Self-recommendations pollute the inbox and create confusing notification badges.
Fix: Add the CHECK constraint at the DB level: ALTER TABLE recommendations ADD CONSTRAINT no_self_recommendation CHECK (sender_id != recipient_id). Additionally, add the guard in the RLS INSERT policy and in the Server Action: if (senderId === auth.uid() && senderId === recipientId) return { error: 'Cannot recommend to yourself' }.
Orphaned recommendations when content is deleted
Symptom: The polymorphic item_id references content in another table (products, articles, posts) but has no FK constraint. When content is deleted, the recommendation row in the inbox shows a dead item_id — the UI either crashes trying to render a null item or shows a blank card.
Fix: Store item_snapshot_title (and optionally item_snapshot_url) in the recommendations row at INSERT time. Render the inbox using item_snapshot_title as the fallback when the live item lookup returns null. For more robust referential integrity, add ON DELETE SET NULL triggers per item_type or implement soft-delete on content items.
Notification badge does not update in real time in V0/Next.js
Symptom: The notification badge lives in the site's root navigation layout (app/layout.tsx in Next.js App Router). V0 commonly generates the badge as a Server Component that fetches the unread count at request time. The Supabase Realtime subscription that makes the badge live requires a 'use client' component, which cannot be the root layout itself.
Fix: Extract the notification badge into a separate 'use client' NotificationBadge component: it initializes the unread count via a Supabase query in useEffect and subscribes to Realtime Postgres Changes on the notifications table filtered by the user's ID. Import it into the layout file — the layout stays a Server Component while the badge alone is a client island.
Recommendation note length not validated — massive text floods inbox
Symptom: AI tools often generate the note textarea without a maxLength attribute or a Zod schema constraint. Users can paste entire articles (10,000+ characters) as recommendation notes. These entries consume disproportionate database space and make the inbox UI break on long-text rendering.
Fix: Add three-layer validation: maxLength={280} on the textarea element, a Zod schema check (z.string().max(280).optional()) on the Server Action, and a database-level constraint CHECK (length(note) <= 280) on the recommendations table.
Recommendations inbox is slow at scale due to missing indexes
Symptom: SELECT * FROM recommendations WHERE recipient_id = $1 AND dismissed_at IS NULL ORDER BY created_at DESC without an index performs a full table scan once the recommendations table has tens of thousands of rows. At 10,000 users who recommend frequently, this query takes 2-5 seconds.
Fix: Add a composite index: CREATE INDEX recommendations_recipient_idx ON recommendations (recipient_id, created_at DESC). Add a partial index for the unread count: CREATE INDEX recommendations_recipient_unread_idx ON recommendations (recipient_id, read_at) WHERE read_at IS NULL. Both are included in the SQL schema above.
Best practices
Always enforce sender_id != recipient_id both as a database CHECK constraint and in the Server Action — the UI guard alone is insufficient and easily bypassed
Store item_snapshot_title at INSERT time so the recommendations inbox remains meaningful even after content deletion — never rely solely on a live item lookup to render the inbox
Use Fuse.js client-side fuzzy search with a preloaded users array instead of running a Supabase query on every keystroke in the recipient combobox — the preloaded approach is faster and doesn't hit Supabase rate limits
Add a 300ms debounce on the Fuse.js search input so the results list doesn't flicker as the user types
Index (recipient_id, created_at DESC) and add a partial index on (recipient_id, read_at) WHERE read_at IS NULL — the unread count query runs on every notification badge render and must be fast
Send the weekly email digest only to users with at least one unread recommendation — never send an empty 'You have 0 recommendations' email
Show an empty state with a CTA on the recommendations inbox ('No recommendations yet — start exploring content and recommend things to your network') to drive engagement rather than showing a blank list
When You Need Custom Development
AI tools handle simple user-to-user recommendation sending and inboxes well. Move to custom when:
- Collaborative filtering is required — surfacing items to users based on what similar-taste users have recommended — this needs ML infrastructure and trained models beyond what AI tools scaffold
- CRM integration to track recommendation-driven conversions: linking a recommendation event to a purchase or signup in HubSpot or Salesforce requires custom webhook and attribution logic
- Reputation scoring for top recommenders as a gamification layer: a leaderboard of most influential recommenders with badges, points, and conversion rate tracking requires a custom analytics pipeline
- Multi-step recommendation flow with privacy controls for private accounts: the sender must be a confirmed follower, the recipient must accept the recommendation before content is revealed — this approval workflow is significantly more complex than AI tools produce
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 is the difference between a recommendation and a referral?
A recommendation is a social, content-focused action: one user shares an item they like with another user, no reward involved. A referral is a business incentive mechanism: user A recruits user B to sign up, and user A gets a reward when user B converts. They use different data models — recommendations track item + note, referrals track conversion events and reward payouts. Build them as separate features; they share no code.
How do I show users who recommended something to them?
Every recommendation row stores sender_id. Join recommendations to a profiles or users table on sender_id to get the display name and avatar. In the recommendations inbox, render the sender's avatar and name on each card. You can also add an 'X people recommended this' count on the item detail page by querying SELECT COUNT(*), array_agg(sender_id) FROM recommendations WHERE item_id = $1 AND item_type = $2.
Can recommendations work for multiple content types (products, posts, users)?
Yes — the polymorphic (item_type, item_id) pattern handles this. Store item_type as a text enum ('product', 'article', 'user', 'post') and item_id as a UUID that references the appropriate table. The recommendations table has no foreign keys to content tables; instead, when rendering the inbox you do a lookup per item_type to fetch the current item data (or fall back to item_snapshot_title if the item was deleted).
How do I prevent spam recommendations?
Add a rate limit: in the Server Action that handles recommendation inserts, query the count of recommendations sent by sender_id in the last 24 hours. If count >= 20 (or your chosen limit), reject the insert with a user-friendly message. Store the limit config in an app_config table so you can adjust it without redeployment. Also enforce unique constraint on (sender_id, recipient_id, item_id, item_type) to prevent the same recommendation being sent repeatedly.
Can I track whether a recommendation led to a purchase?
Yes — add a conversion_event_at column to the recommendations table and a conversion_type column ('view', 'purchase', 'save'). When the recipient views, purchases, or saves the recommended item, call a Server Action that sets conversion_event_at = now() and conversion_type on the relevant recommendation row. Compute conversion rate per sender using: SELECT COUNT(*) FILTER (WHERE conversion_event_at IS NOT NULL) / COUNT(*) FROM recommendations WHERE sender_id = $1.
How do I build a recommendations inbox similar to LinkedIn's?
The LinkedIn pattern is: left panel (nav badge with unread count, links to inbox) and right content area (full recommendation list with sender, item preview, note, and action buttons). Build it as a /recommendations page with a badge in the main nav. Group into 'New' (read_at IS NULL) and 'Earlier' sections. Each card has sender avatar, item preview thumbnail, note, timestamp, and action buttons (View item, Dismiss). Mark read automatically on row click via a Server Action UPDATE.
Should recommendations be private or visible to everyone?
Recommendations are inherently private (sender to specific recipient) in this model. If you want public recommendations visible to all users (e.g., 'Alex recommends this book'), that is a review or endorsement feature with a different schema — a public recommendations table with no recipient_id, displayed on the item detail page. You can build both: private recommendations to a friend and a separate public endorsement.
How do I send email notifications for new recommendations?
In the Supabase Edge Function that runs on recommendation INSERT, after inserting the notification row, call the Resend API with a React Email template. The email should include the sender's name, the item title (from item_snapshot_title), the note, and a 'View in app' link to /recommendations?highlight={recommendationId}. Set the reply-to header to a no-reply address and include an unsubscribe link. Use Resend's free tier (3,000 emails/mo) for apps under 1,000 active users.
Need this feature production-ready?
RapidDev builds an user to user recommendation system into real apps — auth, database, payments — at $13K–$25K.
Book a free consultation30-min call. No commitment.