Feature spec
IntermediateCategory
communication-social
Build with AI
4–7 hours with Lovable or V0
Custom build
1–2 weeks custom dev
Running cost
$0/mo up to 100 users · $25/mo at 1K users
Works on
Everything it takes to ship User Feedback Analytics — parts, prompts, and real costs.
User feedback analytics needs five pieces: an NPS widget (0–10 scale + follow-up textarea) triggered by behavior, a Supabase table storing responses with a materialized view computing correct NPS, a Recharts dashboard showing trends over time, a tagging UI for admins, and a CSV export. With V0 or Lovable you can ship the full feature in 4–7 hours for $0/month at 100 users. Running cost rises to $25/month at 1,000 users when Supabase Pro is needed for materialized view performance.
What User Feedback Analytics Actually Is
User feedback analytics closes the loop between what your app does and what users think of it. The most widely used format is NPS (Net Promoter Score) — an 11-point scale (0–10) with a conditional follow-up question — but the feature is only valuable when paired with: behavioral triggers that show the survey at the right moment, a database that stores every response with enough metadata to segment by cohort or feature, a dashboard that visualizes trends over time rather than point-in-time snapshots, and a close-the-loop mechanism so you can reply to users who submitted negative feedback. The NPS formula itself is a common source of bugs in AI-generated builds, which makes this feature worth prompting carefully.
What users consider table stakes in 2026
- NPS survey appears at a behavior-triggered moment (after completing a task, after N sessions) not on first visit — showing it too early guarantees low response rates
- Conditional follow-up question that changes based on score: 0–6 asks 'what went wrong?', 7–8 asks 'what could be better?', 9–10 asks 'what do you love most?'
- Survey shown no more than once per 30 days to the same user — tracked server-side, not just in browser state
- Admin dashboard with a trend chart showing NPS over the last 90 days, score distribution histogram, and a filterable table of individual responses
- Per-response tagging UI so admins can categorize feedback (bug report, feature request, UX issue) for pattern analysis
- CSV export of all responses with date, score, category, response text, and tags for external analysis
Anatomy of the Feature
Six components across four layers. The NPS formula and the survey throttle logic are the two places AI tools most frequently generate subtly broken behavior.
Feedback widget overlay
UIA shadcn/ui Sheet or Popover that slides in from the bottom edge of the screen. Contains: 11 RadioGroup buttons (0–10) for the NPS score, a conditional follow-up Textarea that appears after the user selects a score (question text changes based on score range), a Submit button, and an X close button. On mobile, checks window.innerHeight > 500 before displaying to avoid covering content on small screens.
Note: The widget must not reappear within 30 days of submission or dismissal. Track this server-side in user_survey_state — never trust browser localStorage alone, which resets on new devices or incognito mode.
Survey trigger engine
BackendA Supabase Edge Function (or a server-side check on page load) that evaluates trigger conditions: has the user completed N sessions this month? Did they just use feature X for the first time? It reads user_survey_state to check last_shown_at and dismissed_count before allowing the widget to display. Sets last_shown_at on both submission and dismissal to reset the 30-day clock.
Note: Never implement only client-side throttling — a user who clears browser data, switches devices, or opens an incognito tab will see the survey again. The user_survey_state table is the single source of truth.
Analytics dashboard
UIBuilt with Recharts: an AreaChart showing NPS trend over last 90 days (weekly aggregated NPS points), a BarChart for score distribution (count of 0s, 1s, ... 10s), and a shadcn/ui DataTable showing individual responses with columns for date, score, category, response excerpt, and tags. Filter controls: date range picker, category dropdown, score range (Detractors / Passives / Promoters). In Next.js, all Recharts components must be loaded with dynamic import and ssr: false.
Note: Wrap every Recharts component in dynamic(() => import('recharts').then(m => ({ default: m.ComponentName })), { ssr: false }) — Recharts accesses window on import and crashes Next.js SSR builds.
NPS score calculator
DataA Supabase materialized view computing the correct NPS formula: ((count of scores 9–10) - (count of scores 0–6)) / (total responses) × 100. Stored as an integer and refreshed daily via a Supabase scheduled function. The materialized view also produces weekly NPS slices (the last 90 days broken into 13 weekly buckets) for the trend chart.
Note: The correct formula subtracts Detractors (0–6) from Promoters (9–10) as a percentage of total responses including Passives (7–8). AI tools frequently compute (Promoters / Total) × 100 without subtracting Detractors — this inflates scores significantly and is the single most common bug in AI-generated NPS builds.
Feedback tagging system
DataA feedback_tags table with a many-to-many relationship to feedback rows. The admin UI shows a multi-select tag dropdown on each response in the DataTable. An optional auto-tagging Edge Function applies keyword matching rules (e.g. if body contains 'slow' or 'loading' → tag as 'performance') on insert and can be extended with an OpenAI API call for smarter categorization.
Note: Store tags as an array column on the feedback table (tags text[]) in addition to the junction table — this makes filtering by tag in the dashboard a simple array overlap query without a JOIN.
CSV export
BackendA Supabase Edge Function that streams a cursor-based SELECT query to a CSV response with Content-Disposition: attachment; filename='feedback.csv'. Fetches rows in batches of 1,000 using cursor-based pagination (ORDER BY created_at, WHERE created_at > last_cursor) to avoid loading 50K rows into memory at once. Columns: date, nps_score, category, body, tags.
Note: Do not buffer the entire result set in a single array inside the Edge Function — at 50K rows this hits Supabase Edge Function memory limits. The streaming cursor pattern keeps memory usage constant regardless of dataset size.
The data model
Three tables and one materialized view are needed. Run this in the Supabase SQL Editor — the materialized view includes the correct NPS formula so your dashboard numbers are accurate from day one:
1create table public.feedback (2 id uuid primary key default gen_random_uuid(),3 user_id uuid references auth.users(id) on delete set null,4 nps_score int not null check (nps_score >= 0 and nps_score <= 10),5 category text,6 body text,7 sentiment text,8 tags text[] not null default '{}',9 created_at timestamptz not null default now()10);1112alter table public.feedback enable row level security;1314-- Users can submit feedback15create policy "Authenticated users can submit feedback"16 on public.feedback for insert17 with check (auth.uid() = user_id or user_id is null);1819-- Admins can read all feedback (requires admin role check)20create policy "Users can read own feedback"21 on public.feedback for select22 using (auth.uid() = user_id);2324create index feedback_created_category_idx25 on public.feedback (created_at desc, category);2627create index feedback_tags_gin_idx28 on public.feedback using gin (tags);2930-- Survey throttle tracking31create table public.user_survey_state (32 user_id uuid primary key references auth.users(id) on delete cascade,33 last_shown_at timestamptz,34 times_shown int not null default 0,35 dismissed_count int not null default 036);3738alter table public.user_survey_state enable row level security;3940create policy "Users can manage own survey state"41 on public.user_survey_state for all42 using (auth.uid() = user_id)43 with check (auth.uid() = user_id);4445-- Close-the-loop responses from admins46create table public.feedback_responses (47 id uuid primary key default gen_random_uuid(),48 feedback_id uuid references public.feedback(id) on delete cascade not null,49 admin_id uuid references auth.users(id) on delete set null,50 response_body text not null,51 sent_at timestamptz not null default now()52);5354alter table public.feedback_responses enable row level security;5556-- NPS materialized view: correct formula57create materialized view public.nps_summary as58select59 date_trunc('week', created_at) as week_start,60 count(*) as total_responses,61 count(*) filter (where nps_score >= 9) as promoters,62 count(*) filter (where nps_score <= 6) as detractors,63 count(*) filter (where nps_score between 7 and 8) as passives,64 round(65 (count(*) filter (where nps_score >= 9)::numeric -66 count(*) filter (where nps_score <= 6)::numeric) /67 nullif(count(*), 0)::numeric * 10068 ) as nps_score69from public.feedback70group by date_trunc('week', created_at)71order by week_start desc;7273create unique index nps_summary_week_idx on public.nps_summary (week_start);7475-- Refresh daily via pg_cron (enable pg_cron extension first)76-- SELECT cron.schedule('refresh-nps', '0 3 * * *', 'REFRESH MATERIALIZED VIEW CONCURRENTLY public.nps_summary');Heads up: The materialized view uses CONCURRENTLY refresh (requires the unique index) so the dashboard never shows a blank chart during a refresh. Enable the pg_cron extension in Supabase Dashboard → Database → Extensions to schedule automatic daily refreshes.
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.
Best path for the analytics dashboard — V0 generates Recharts components with full TypeScript types and Server Actions for feedback submission. The data visualization output is consistently higher quality than other AI tools for this use case.
Step by step
- 1Prompt V0 with the spec below to generate the NPS widget, the admin dashboard with Recharts charts, and the Server Actions
- 2Add environment variables in the Vars panel: NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, and RESEND_API_KEY for close-the-loop emails
- 3Run the SQL schema from this page in the Supabase SQL Editor — pay attention to the materialized view with the correct NPS formula
- 4Enable pg_cron in Supabase Dashboard → Database → Extensions and run the cron.schedule() call from the schema comments to auto-refresh the view daily
- 5Deploy to Vercel and test the full flow: submit a score as a regular user, then open the admin dashboard to verify the NPS trend chart and the response row
Create a user feedback analytics feature for a Next.js 14 App Router project. Part 1 — NPS widget (client component FeedbackWidget): render only when the user has not submitted or dismissed the survey in the last 30 days (check Supabase user_survey_state.last_shown_at server-side before sending to client). Widget: shadcn/ui Sheet sliding from bottom, 11 RadioGroup buttons labeled 0–10 for NPS score, conditional follow-up Textarea (score 0-6: 'What went wrong?', 7-8: 'What could be better?', 9-10: 'What do you love most?'), Submit and Dismiss buttons. On submit: POST to /api/feedback with { nps_score, body, category }; update user_survey_state.last_shown_at to now(). Do not show widget if window.innerHeight < 500. Part 2 — Admin dashboard page /admin/feedback (server component wrapping client charts): use Recharts AreaChart showing weekly NPS from nps_summary materialized view (last 90 days / 13 weeks). Wrap Recharts in dynamic import with ssr:false. BarChart for score distribution (0–10 counts). shadcn DataTable showing individual feedback rows (date, nps_score, category, body excerpt, tags). Filter controls: date range picker, category select, score range filter (Detractors 0-6 / Passives 7-8 / Promoters 9-10). Tags column with multi-select admin tagging via PATCH /api/feedback/:id/tags. CSV export button calling GET /api/feedback/export which streams cursor-based SELECT. Part 3 — Close-the-loop: each DataTable row has a Reply button opening a shadcn Dialog with a textarea; on submit, POST to /api/feedback/:id/reply which inserts to feedback_responses and calls Resend API to email the feedback submitter. Empty state: 'No feedback yet — share your app with users to start collecting responses.'Where this path bites
- Recharts SSR crash is certain if dynamic import with ssr:false is not used — V0 sometimes generates static Recharts imports that break the build
- The NPS materialized view is not auto-created by V0 — run the SQL from this page manually in Supabase
- Survey trigger logic (behavioral conditions beyond 30-day throttle) requires a custom API route that V0 does not generate by default
Third-party services you'll need
Core feedback collection and analytics run on Supabase alone. Optional services add email and richer visualization:
| Service | What it does | Free tier | Paid from |
|---|---|---|---|
| Supabase | PostgreSQL database, materialized NPS view, Edge Functions for export and triggers, RLS | 500MB DB, 2 projects | $25/mo (Pro) |
| Resend | Close-the-loop reply emails from admin to feedback submitters | 3,000 emails/mo | $20/mo for 50K emails (approx) |
| Recharts | NPS trend AreaChart and score distribution BarChart in the admin dashboard | Free (open source) | $0 |
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 and materialized view at this scale. Resend free tier (3K emails) handles all close-the-loop replies. Recharts is free.
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.
NPS survey shown repeatedly to the same user
Symptom: AI tools commonly store last_shown_at in React component state or browser localStorage. Both reset on page reload, device switch, or clearing browser data — the survey reappears every visit. Users who see the NPS prompt repeatedly report it as the most annoying UX pattern possible.
Fix: Persist last_shown_at exclusively in the Supabase user_survey_state table. Check it server-side (in a Server Action or API route) before rendering the FeedbackWidget component. If last_shown_at is within 30 days, return null from the server — never render the widget at all.
NPS formula calculated incorrectly
Symptom: The correct NPS formula is ((Promoters - Detractors) / Total) × 100 where Promoters = scores 9–10 and Detractors = scores 0–6. AI tools frequently generate (Promoters / Total) × 100, omitting the Detractor subtraction. This produces inflated scores — an app with 50% Promoters and 40% Detractors shows NPS of +50 instead of the correct +10.
Fix: Validate the materialized view formula against known test data: seed 10 responses (5 Promoters, 2 Passives, 3 Detractors) and verify the computed NPS is +20, not +50. The correct SQL expression is ROUND((promoters::numeric - detractors::numeric) / NULLIF(total_responses, 0)::numeric * 100).
CSV export crashes on large datasets
Symptom: A naive Edge Function runs SELECT * FROM feedback with no limit and buffers all rows into a JavaScript array before streaming to CSV. At 50,000 rows this hits Supabase Edge Function memory limits and the export returns a 500 error — right when your biggest customers need their data.
Fix: Use cursor-based pagination: SELECT id, nps_score, body, tags, created_at FROM feedback WHERE created_at > $last_cursor ORDER BY created_at LIMIT 1000. Write each batch to the response stream immediately, then advance the cursor. This keeps memory usage constant at roughly 1,000 rows × row size regardless of total dataset.
Recharts SSR crash in Next.js
Symptom: Recharts accesses the browser window and document objects during module initialization. Next.js runs module imports server-side during the build, throwing 'window is not defined' and causing the entire admin dashboard page to fail to render or build.
Fix: Wrap every Recharts component in Next.js dynamic import with ssr disabled: const AreaChart = dynamic(() => import('recharts').then(m => ({ default: m.AreaChart })), { ssr: false }). Apply this to every Recharts export used in the file — a single missed component will still crash the build.
Feedback widget covers content on mobile
Symptom: A fixed bottom Sheet overlay covers the active screen content on small phones (iPhone SE, Android budget devices under 670px height). Users on these devices cannot see what they are rating and cannot easily close the widget, leading to force-closes and lost feedback.
Fix: Add a minimum viewport height check before rendering the widget: if (window.innerHeight < 500) return null. For borderline sizes (500–650px), render a more compact single-line NPS row instead of the full Sheet. Always include a clearly visible X close button positioned at the top-right of the overlay, not at the bottom where it may be cut off.
Best practices
Track survey display state in Supabase user_survey_state, never in browser storage — localStorage resets on new devices and breaks your 30-day throttle
Validate your NPS formula with a known test dataset immediately after build — the (Promoters - Detractors) / Total formula is easy to implement incorrectly and hard to notice from the chart alone
Use a Supabase materialized view with CONCURRENTLY refresh for the NPS trend calculation rather than computing it on every dashboard page load — a COUNT + FILTER query over 50K rows on every admin visit adds unnecessary latency
Make close-the-loop reply emails personal and specific — a response that references the actual feedback text converts Detractors into Promoters at a measurably higher rate than generic acknowledgments
Show the conditional follow-up question (0–6: what went wrong, 7–8: what could improve, 9–10: what do you love) — open-ended context transforms a number into actionable product intelligence
Stream the CSV export with cursor-based pagination rather than buffering all rows — this prevents memory errors at scale and lets large exports complete without timeout
Add a minimum screen height check before displaying the feedback overlay on mobile — covering the entire screen on a small phone is the fastest way to earn a 1-star App Store review
When You Need Custom Development
AI tools handle standard NPS collection and a Recharts dashboard well. These requirements exceed what a first-build session produces reliably:
- Sentiment analysis on feedback text is required — scoring emotion and tone via AWS Comprehend or OpenAI API and auto-categorizing responses beyond a keyword blocklist
- Cohort analysis is needed — correlating NPS scores with user acquisition source, plan tier, or onboarding completion rate to understand which cohorts are most at-risk of churn
- Automated churn prediction based on declining NPS scores with trigger-based outreach sequences to at-risk accounts
- CRM integration (Salesforce, HubSpot) is required to push each feedback row to the corresponding contact record so Customer Success teams see NPS alongside account health in one place
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 I calculate NPS correctly in my app?
NPS = ((count of scores 9 or 10) - (count of scores 0 through 6)) / (total responses including 7s and 8s) × 100. Scores 7–8 are Passives — they count in the denominator but not in the numerator. The result ranges from -100 (all Detractors) to +100 (all Promoters). Validate with test data: if you have 5 Promoters, 2 Passives, and 3 Detractors out of 10 total, NPS should be (5-3)/10 × 100 = +20.
How often should I show the NPS survey to the same user?
30 days is the industry standard minimum between surveys. Showing it more frequently reduces response quality and annoys users — you will see lower scores simply from survey fatigue. Store the last_shown_at timestamp in your database (not localStorage) so it persists across devices and browser clears. Only reset the 30-day clock on submission or explicit dismissal — not on page refresh.
How do I segment feedback by feature or user cohort?
Add a category column to your feedback table and populate it from the widget's context — pass the current page or feature name as category when inserting the feedback row (e.g. category = 'onboarding', 'checkout', 'dashboard'). In the admin dashboard, add a category filter to the DataTable and AreaChart. For cohort segmentation (plan tier, signup month), join the feedback table with your users table on user_id and add those columns to the materialized view.
Can I reply to users who leave negative feedback?
Yes — this is called 'closing the loop' and is one of the highest-ROI actions in NPS programs. Add a feedback_responses table and a Reply button in the admin DataTable that opens a textarea dialog. On submit, insert to feedback_responses and call the Resend API to send an email to the feedback submitter. Resend's free tier covers 3,000 emails/month (approx), more than enough for most teams starting out.
How do I export feedback responses to CSV?
Create a Supabase Edge Function that runs a cursor-based SELECT query over your feedback table and streams the result as a CSV file with a Content-Disposition: attachment; filename='feedback.csv' response header. Fetch rows in batches of 1,000 using WHERE created_at > last_cursor to avoid loading the entire table into memory. Add an Export button in the admin dashboard that calls this endpoint directly — the browser will handle the file download.
How do I add auto-tagging to categorize feedback automatically?
Start with keyword matching in a Supabase Edge Function triggered on INSERT: if the feedback body contains terms like 'slow', 'loading', or 'timeout', add the 'performance' tag to the tags array. For more nuanced classification, POST the feedback body to OpenAI or Claude with a prompt asking it to assign one of your predefined category tags. Store the AI-assigned tags alongside manual admin tags in the same tags column.
What is the difference between NPS and CSAT?
NPS (Net Promoter Score) measures loyalty and likelihood to recommend — it is a single 0–10 question asked periodically. CSAT (Customer Satisfaction Score) measures satisfaction with a specific interaction — typically a 1–5 star rating asked immediately after a support ticket or purchase. NPS is better for tracking overall product health over time. CSAT is better for evaluating individual touchpoints. For most early-stage apps, NPS provides more actionable insight with less survey frequency.
How do I trigger the feedback widget based on user behavior?
Create a Supabase Edge Function or server-side check that evaluates trigger conditions before rendering the widget: has the user completed 3 or more sessions? Did they just complete their first successful export or purchase? Have 30 days passed since their last survey? Check these conditions on page load using a Server Action in Next.js, then pass a boolean showFeedbackWidget prop to the client component. This keeps trigger logic server-side and lets you adjust conditions without a frontend redeploy.
Need this feature production-ready?
RapidDev builds user feedback analytics into real apps — auth, database, payments — at $13K–$25K.
Book a free consultation30-min call. No commitment.