# How to Add User Feedback Analytics to Your App (Copy-Paste Prompts Included)

- Tool: App Features
- Last updated: July 2026

## TL;DR

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.

## 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** (ui): A 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.
- **Survey trigger engine** (backend): A 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.
- **Analytics dashboard** (ui): Built 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.
- **NPS score calculator** (data): A 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.
- **Feedback tagging system** (data): A 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.
- **CSV export** (backend): A 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.

## 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:

```sql
create table public.feedback (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete set null,
  nps_score int not null check (nps_score >= 0 and nps_score <= 10),
  category text,
  body text,
  sentiment text,
  tags text[] not null default '{}',
  created_at timestamptz not null default now()
);

alter table public.feedback enable row level security;

-- Users can submit feedback
create policy "Authenticated users can submit feedback"
  on public.feedback for insert
  with check (auth.uid() = user_id or user_id is null);

-- Admins can read all feedback (requires admin role check)
create policy "Users can read own feedback"
  on public.feedback for select
  using (auth.uid() = user_id);

create index feedback_created_category_idx
  on public.feedback (created_at desc, category);

create index feedback_tags_gin_idx
  on public.feedback using gin (tags);

-- Survey throttle tracking
create table public.user_survey_state (
  user_id uuid primary key references auth.users(id) on delete cascade,
  last_shown_at timestamptz,
  times_shown int not null default 0,
  dismissed_count int not null default 0
);

alter table public.user_survey_state enable row level security;

create policy "Users can manage own survey state"
  on public.user_survey_state for all
  using (auth.uid() = user_id)
  with check (auth.uid() = user_id);

-- Close-the-loop responses from admins
create table public.feedback_responses (
  id uuid primary key default gen_random_uuid(),
  feedback_id uuid references public.feedback(id) on delete cascade not null,
  admin_id uuid references auth.users(id) on delete set null,
  response_body text not null,
  sent_at timestamptz not null default now()
);

alter table public.feedback_responses enable row level security;

-- NPS materialized view: correct formula
create materialized view public.nps_summary as
select
  date_trunc('week', created_at) as week_start,
  count(*) as total_responses,
  count(*) filter (where nps_score >= 9) as promoters,
  count(*) filter (where nps_score <= 6) as detractors,
  count(*) filter (where nps_score between 7 and 8) as passives,
  round(
    (count(*) filter (where nps_score >= 9)::numeric -
     count(*) filter (where nps_score <= 6)::numeric) /
    nullif(count(*), 0)::numeric * 100
  ) as nps_score
from public.feedback
group by date_trunc('week', created_at)
order by week_start desc;

create unique index nps_summary_week_idx on public.nps_summary (week_start);

-- Refresh daily via pg_cron (enable pg_cron extension first)
-- SELECT cron.schedule('refresh-nps', '0 3 * * *', 'REFRESH MATERIALIZED VIEW CONCURRENTLY public.nps_summary');
```

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 paths

### V0 — fit 5/10, 4–6 hours

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.

1. Prompt V0 with the spec below to generate the NPS widget, the admin dashboard with Recharts charts, and the Server Actions
2. Add environment variables in the Vars panel: NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, and RESEND_API_KEY for close-the-loop emails
3. Run the SQL schema from this page in the Supabase SQL Editor — pay attention to the materialized view with the correct NPS formula
4. Enable pg_cron in Supabase Dashboard → Database → Extensions and run the cron.schedule() call from the schema comments to auto-refresh the view daily
5. Deploy 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

Starter prompt:

```
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.'
```

Limitations:

- 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

### Lovable — fit 4/10, 5–7 hours

Good for the feedback widget UI and Supabase storage integration. The NPS trend chart with Recharts requires explicit prompting and the materialized view must be created manually in the Supabase SQL Editor.

1. Create a new Lovable project, connect Lovable Cloud for Supabase Auth and database access
2. Open Cloud tab → Secrets and add RESEND_API_KEY for close-the-loop reply emails
3. Run the SQL schema from this page in Cloud tab → Database → SQL Editor, including the materialized view and pg_cron schedule
4. Paste the prompt below in Agent Mode to build the widget, admin dashboard, and export
5. After the build, send a follow-up prompt targeting the NPS formula: 'Verify the NPS score calculation uses ((Promoters - Detractors) / Total) × 100 where Promoters = scores 9–10 and Detractors = scores 0–6'

Starter prompt:

```
Build a user feedback analytics feature. Data: Supabase tables 'feedback' (id, user_id, nps_score 0-10, category text, body text, tags text[], created_at) and 'user_survey_state' (user_id, last_shown_at, times_shown, dismissed_count). UI 1 — FeedbackWidget component: shadcn Sheet from bottom. Only render after checking Supabase user_survey_state — if last_shown_at is within 30 days, do not show. Inside the Sheet: 11 RadioGroup buttons (0–10) for NPS, conditional follow-up Textarea (0-6: 'What went wrong?', 7-8: 'What could be better?', 9-10: 'What do you love most?'), Submit and Dismiss buttons. On submit: insert to feedback table, upsert user_survey_state with last_shown_at = now(). Check window.innerHeight > 500 before showing. UI 2 — Admin feedback dashboard: Recharts AreaChart showing weekly NPS trend from Supabase query of feedback table grouped by week (use correct NPS formula: ((promoters - detractors) / total) × 100 where promoters = score >= 9, detractors = score <= 6). shadcn DataTable of individual responses with date, score, category, body, tags columns. Multi-select tag editor on each row. Filter dropdown by category and score range. Export button calling Supabase Edge Function 'export-feedback' that returns CSV with columns: date, score, category, response, tags. Close-the-loop reply button on each row sending email via Resend API. Empty state: 'No feedback yet' with call-to-action.
```

Limitations:

- The NPS materialized view with pg_cron auto-refresh must be created manually in the Supabase SQL Editor — Lovable does not provision it
- AI sometimes uses the incorrect NPS formula (Promoters / Total instead of (Promoters - Detractors) / Total) — verify the chart numbers against a known test dataset after the build
- Recharts in Lovable requires dynamic import with ssr:false if you later migrate to Next.js; in Lovable's Vite environment this is less critical but still best practice

### Flutterflow — fit 3/10, 5–8 hours

Works for the feedback widget overlay and Firebase/Supabase storage of responses. The analytics dashboard with trend charts is better built as a web companion since FlutterFlow's charting (fl_chart) is more limited than Recharts.

1. Add a FlutterFlow Popup or BottomSheet widget for the NPS survey overlay
2. Use 11 Container widgets styled as number buttons for the NPS scale; store selected score in a Page State variable
3. Add a conditional TextField that appears based on the score range using an if/else Conditional Widget action
4. Add a Backend Query Action on submit that inserts to your Firebase or Supabase feedback table
5. Check the user_survey_state collection/table before showing the widget using a Backend Query Condition in the page's initState action

Limitations:

- Analytics dashboard with trend charts is limited — fl_chart supports basic line charts but lacks the customization of Recharts; consider building the admin dashboard as a separate web app
- No URL-based export on mobile — CSV export requires a custom Flutter action writing to device storage
- Recharts-based admin dashboard is web-only and requires a separate Lovable or V0 project

### Custom — fit 4/10, 1–2 weeks

Full analytics pipeline with cohort analysis, churn prediction correlation, automated close-the-loop email sequences, and CRM integration. Choose this when feedback data feeds business decisions at scale.

1. Build a behavioral trigger system that evaluates multiple conditions (session count, feature usage, plan tier) to determine NPS timing per user segment
2. Add sentiment analysis on feedback text body using AWS Comprehend or OpenAI API to auto-classify tone and emotion beyond the numeric NPS score
3. Set up automated close-the-loop email sequence via Resend: immediate acknowledgment on submit, follow-up from a team member on Detractor responses within 24 hours
4. Integrate with Salesforce or HubSpot CRM to push each feedback row to the corresponding contact record, enabling CS team to correlate NPS with account health

Limitations:

- Cohort analysis and churn correlation require behavioral event data beyond what a simple feedback table provides — integrate with a dedicated analytics tool or build a parallel events table
- CRM integrations (Salesforce, HubSpot) add complexity and OAuth setup that extends the build timeline significantly

## Gotchas

- **NPS survey shown repeatedly to the same user** — 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** — 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** — 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** — 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** — 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

## 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.

---

Source: https://www.rapidevelopers.com/app-features/user-feedback-analytics
© RapidDev — https://www.rapidevelopers.com/app-features/user-feedback-analytics
