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

- Tool: App Features
- Last updated: July 2026

## TL;DR

A feedback form needs a react-hook-form UI with NPS (0-10) and star rating (1-5) inputs, a Supabase table that accepts both authenticated and anonymous submissions, and RLS that blocks regular users from reading each other's responses. With Lovable you can ship a working form plus an admin view in 1-3 hours for $0/mo — Supabase and Resend free tiers cover most apps indefinitely.

## What a Feedback Form Feature Actually Is

A feedback form is the lightweight channel through which users tell you what is working and what is not — an NPS survey after onboarding, a star rating after a support interaction, an open text box at the bottom of any screen. The data it produces is only useful if you can read it: an admin view to filter by date, rating, and category is as important as the form itself. The architecture is deliberately simple — a validated form, a Supabase INSERT, and RLS that lets anyone submit but restricts reads to admins only. The beginner mistakes are double-submits on mobile and anonymous INSERT policies that the AI forgets to add.

## Anatomy of the Feature

Six components make up a production feedback form. AI tools generate the form UI reliably — the anonymous RLS policy and the admin SELECT restriction are where builds fail.

- **Form UI component** (ui): Built with react-hook-form v7 and zod validation. Uses shadcn/ui Form, Input, Textarea, RadioGroup, and Slider components. The form controller manages the rating value, category selection, and text field — all wired to zod schema for type-safe validation before submission.
- **NPS and star rating widget** (ui): Two separate question types: an NPS selector showing buttons labeled 0-10 with 'Not at all likely' and 'Extremely likely' anchors, and a 1-5 star rating using Lucide Star icons that fill on hover and click. Both store their value as an integer in the same zod-validated field.
- **Submission handler** (backend): A Supabase Server Action or Edge Function that receives the validated form data and INSERTs a row into the feedback table. Rate-limited to one submission per user (or per IP for anonymous) per 24 hours via a unique constraint or RLS check.
- **Response storage** (data): A single Supabase PostgreSQL feedback table with user_id (nullable for anonymous), rating, category, body text, metadata jsonb for custom fields, and created_at. The nullable user_id is what makes anonymous submissions possible.
- **Admin response viewer** (ui): A shadcn/ui DataTable component on an admin-only route showing all feedback rows. Filterable by date range, rating (min/max), and category. The aggregate NPS score (percentage of promoters minus percentage of detractors) is computed in a Supabase SQL view or RPC function and displayed at the top.
- **Email notification on submit** (service): A Supabase Edge Function triggered by an INSERT on the feedback table calls the Resend API to send an email to the admin with the rating, category, and body text. Sent only when rating is below a threshold (e.g., NPS 0-6) to avoid inbox overload for high-volume apps.

## Data model

One table stores all feedback with anonymous-safe RLS. Admin access requires a role check on the JWT. Run this in the Supabase SQL editor.

```sql
create table public.feedback (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete set null,
  rating int not null check (rating >= 0 and rating <= 10),
  category text not null default 'General' check (category in ('Bug Report', 'Feature Request', 'General')),
  body text check (char_length(body) <= 500),
  metadata jsonb default '{}',
  created_at timestamptz not null default now()
);

alter table public.feedback enable row level security;

-- Authenticated users can submit feedback
create policy "authenticated insert"
  on public.feedback for insert
  to authenticated
  with check (auth.uid() = user_id);

-- Anonymous users can submit feedback (user_id will be null)
create policy "anon insert"
  on public.feedback for insert
  to anon
  with check (user_id is null);

-- Only admins can read feedback
create policy "admin select"
  on public.feedback for select
  using (auth.jwt() ->> 'role' = 'admin');

-- Unique constraint prevents duplicate submissions per user per day
create unique index feedback_user_daily_unique
  on public.feedback (user_id, (created_at::date))
  where user_id is not null;

create index feedback_created_at_idx
  on public.feedback (created_at desc);

create index feedback_rating_idx
  on public.feedback (rating);

-- RPC function to compute NPS score
create or replace function public.compute_nps()
returns numeric
language sql
security definer
as $$
  select
    round(
      (
        count(*) filter (where rating >= 9)::numeric
        - count(*) filter (where rating <= 6)::numeric
      ) / nullif(count(*), 0) * 100
    , 1)
  from public.feedback;
$$;
```

The unique index on (user_id, created_at::date) prevents authenticated users from submitting more than once per day at the database level — the application-layer guard is a second line of defence. The compute_nps() RPC function uses security definer to run with elevated privileges so it works even when the admin JWT is not present in a server-side call.

## Build paths

### Lovable — fit 5/10, 1-2 hours

Best path for beginners: Lovable generates the complete form, Supabase table, anonymous RLS, and admin view in a single prompt with no manual wiring required.

1. Create a new Lovable project and connect Lovable Cloud so Supabase is provisioned automatically
2. Paste the prompt below in Agent Mode — Lovable will create the feedback table with RLS, the form component, and the admin view in one pass
3. Open the Cloud tab and verify the feedback table exists with RLS enabled; check that both the authenticated and anon INSERT policies are present
4. Add RESEND_API_KEY to the Secrets panel (Cloud tab) to enable admin email notifications
5. Publish and test by submitting feedback while logged out (anonymous) and while logged in (authenticated)

Starter prompt:

```
Build a feedback form feature using Supabase. Create a feedback table: id (uuid pk), user_id (uuid nullable references auth.users), rating (int 0-10 NPS scale), category (text: 'Bug Report', 'Feature Request', 'General'), body (text max 500 chars), metadata (jsonb), created_at (timestamptz). Enable RLS: authenticated users can INSERT with user_id = auth.uid(); anonymous users (role = anon) can INSERT with user_id null; only users with JWT role = 'admin' can SELECT. Add a unique constraint preventing the same user from submitting more than once per calendar day. UI: Feedback form page with two question types — NPS selector (buttons 0-10 with 'Not at all likely' / 'Extremely likely' labels) and star rating (1-5 Lucide Star icons). Category dropdown with three options. Textarea for qualitative feedback with character counter showing remaining of 500 max. Submit button shows loading state during insert. On success, show a thank-you screen with 'Back to app' link. Wrap NPS and star inputs in react-hook-form Controller so values persist in multi-step forms. Validate with zod: rating required, body optional but max 500 chars. Add a Supabase Edge Function that calls Resend API (RESEND_API_KEY in Secrets) to email the admin when a new submission arrives — include rating, category, and body in the email. Admin page at /admin/feedback: shadcn/ui DataTable listing all rows, filterable by date range and category, showing average NPS score at the top computed via a Supabase RPC function. Handle the empty state when no feedback exists yet.
```

Limitations:

- Multi-step conditional logic (show question B only if answer to A is X) requires manual refinement — Lovable's first pass usually generates all questions on a single screen
- The admin JWT role check assumes you have set a custom role claim in Supabase Auth; if not, admin access requires a separate profiles table with an is_admin boolean

### V0 — fit 4/10, 2-3 hours

Excellent for polished form UI in an existing Next.js app. v0 generates clean shadcn/ui form components; wiring to Supabase requires manual env var setup.

1. Prompt v0 with the spec below to generate the FeedbackForm and AdminFeedbackTable components
2. Open the Vars panel and add NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY
3. Run the SQL schema from this page in the Supabase SQL editor to create the feedback table and RLS policies
4. Add a Server Action or Route Handler for the form submission to keep the Supabase service key server-side
5. Deploy to Vercel and test anonymous submission by opening the form in an incognito window

Starter prompt:

```
Build a feedback form feature for a Next.js 14 App Router app using Supabase. Components: FeedbackForm (client component) — react-hook-form v7 with zod validation. NPS question: buttons 0-10 with 'Not likely' and 'Extremely likely' labels, managed via Controller. Star rating question: 1-5 Lucide Star icons, fill on hover and click, managed via Controller. Category select: shadcn/ui Select with options Bug Report, Feature Request, General. Textarea with character counter (max 500 chars). Submit button with loading state. Success state: thank-you message with 'Submit another' link. AdminFeedbackTable (server component) — fetches feedback rows using Supabase server client with service role key; shadcn/ui DataTable with columns for date, rating, category, body; date range filter; average NPS displayed above table computed via compute_nps() RPC. Wire FeedbackForm to a Server Action that INSERTs into the feedback table — anonymous users send user_id as null. Include TypeScript types for FeedbackRow. Supabase table: feedback with RLS — anon INSERT allowed with user_id null; authenticated INSERT with user_id = auth.uid(); SELECT only for admin role.
```

Limitations:

- Supabase is not auto-provisioned — create the schema manually in the Supabase dashboard before testing
- Anonymous INSERT requires the anon role INSERT policy; v0 sometimes generates only the authenticated policy — verify both exist
- Admin table requires the Supabase service role key on the server — never expose it in a client component

### Flutterflow — fit 3/10, 4-8 hours

Works well for a simple rating + text form on mobile with Firebase Firestore as the native backend. Complex multi-step logic requires custom Dart actions.

1. Create a Firestore collection 'feedback' with fields: userId (string nullable), rating (integer), category (string), body (string), createdAt (timestamp)
2. Build a FlutterFlow page with a Column widget containing a Rating widget (1-5 stars, built-in), a DropdownButton for category, and a TextField with maxLength 500
3. Add a Firestore Create Document action on the submit button wired to the feedback collection
4. Add a Conditional Visibility widget to show the thank-you state after successful submission
5. For the admin view, create a separate page visible only to users with the admin custom claim, with a Firestore collection query ordered by createdAt desc

Limitations:

- No built-in NPS 0-10 widget — requires a Row of 11 TextButton widgets or a custom Dart widget
- Complex multi-step conditional logic requires custom Dart actions; the FlutterFlow UI builder cannot express skip logic
- No built-in admin dashboard — must build a separate FlutterFlow page with a Firestore query filtered by role

### Custom — fit 2/10, 3-5 days

Only justified for GDPR data residency requirements, complex branching survey logic with 10+ questions, or CRM integration via webhooks.

1. GDPR data residency: self-host PostgreSQL in an EU region with right-to-erasure workflow that deletes all feedback rows for a given user_id on request
2. Complex branching: use a form schema stored in JSONB that defines question order and skip conditions, rendered by a dynamic form engine
3. CRM integration: webhook on INSERT calls HubSpot or Salesforce API to create a contact activity with the feedback score
4. Sentiment analysis pipeline: on INSERT, call an Anthropic or OpenAI API with the body text and store a sentiment score in the metadata column

Limitations:

- 3-5 days of development for a feature Lovable ships in 1-2 hours — only justified when compliance or CRM integration is a hard requirement

## Gotchas

- **Form submits twice on mobile** — React StrictMode double-invokes effects and event handlers in development — combined with a slow network causing the user to tap Submit twice, the Edge Function receives two identical POST requests within milliseconds. The result is two identical feedback rows with the same user_id and timestamp. Fix: Add a client-side isSubmitting boolean guard that sets true on first submit and prevents a second call until the promise resolves. At the database level, add a unique index on (user_id, (created_at::date)) and use INSERT ... ON CONFLICT DO NOTHING so even if a duplicate reaches the server, only one row is inserted.
- **Anonymous submissions blocked by RLS** — AI-generated RLS typically includes only an authenticated INSERT policy that checks auth.uid() IS NOT NULL. Anonymous users making requests with the Supabase anon key are assigned the 'anon' role — they have no uid, so the policy rejects them with 'new row violates row-level security policy' even though no login should be required. Fix: Add a separate anon policy: CREATE POLICY "anon insert" ON feedback FOR INSERT TO anon WITH CHECK (user_id IS NULL). Make sure user_id is nullable in the table schema. Test by submitting the form in an incognito window without logging in.
- **Admin table shows all feedback to any authenticated user** — AI generates a SELECT policy for 'authenticated' role without restricting to admins — any logged-in user navigating to /admin/feedback can read every response. This is a data privacy issue, not just a UX problem. Fix: Replace the broad SELECT policy with one that checks the admin role: CREATE POLICY "admin select" ON feedback FOR SELECT USING (auth.jwt() ->> 'role' = 'admin'). Set the admin role claim in Supabase Auth using a custom JWT hook or by storing it in the profiles table and reading it server-side with the service role key.
- **Star rating resets on re-render** — Custom star rating components built as uncontrolled React elements (internal useState without react-hook-form integration) lose their value whenever the parent form re-renders — for example, when the character counter updates on each keystroke. The user sees their 5-star rating reset to 0 while typing their comment. Fix: Wrap the star rating widget in a react-hook-form Controller component and use the field.value and field.onChange props to keep the value in the form state instead of local component state. Set defaultValue on the Controller.

## Best practices

- Always write both an authenticated INSERT policy and a separate anon INSERT policy — anonymous feedback is often your most candid signal
- Add a unique constraint at the database level on (user_id, created_at::date) to prevent duplicate submissions even if your client-side guard fails
- Restrict admin SELECT with a role check, not just route-level authentication — any authenticated user who discovers the admin URL should not see all feedback
- Wrap all custom rating inputs in react-hook-form Controller — uncontrolled components lose state on any parent re-render
- Send admin email notifications only for low-rating submissions (NPS 0-6 or 1-2 stars) to keep the signal-to-noise ratio high
- Store metadata (app version, device type, current page) in the jsonb column from day one — you will want to correlate feedback with context later
- Show a character counter on the open text field from the start — without it, users either write nothing or hit an invisible limit

## Frequently asked questions

### What is the simplest way to add a feedback form to a Lovable app?

One Lovable prompt covers the full feedback form: specify NPS scale (0-10), star rating (1-5), a category dropdown, an open text field with a 500-character limit, and a thank-you screen. Lovable creates the Supabase table, the RLS policies, and the form component automatically. The entire build takes 1-2 hours.

### Can users submit feedback anonymously?

Yes — make user_id nullable in the feedback table and add a separate Supabase RLS policy for the anon role: CREATE POLICY "anon insert" ON feedback FOR INSERT TO anon WITH CHECK (user_id IS NULL). Without this second policy, the default authenticated-only INSERT policy blocks all logged-out submissions. Test by submitting in an incognito window.

### How do I see all feedback responses in one place?

Add an admin page with a shadcn/ui DataTable component that reads from the feedback table using the Supabase service role key (server-side only). Apply date range and category filters and display the aggregate NPS score at the top via a SQL RPC function. Restrict access to this page with a role check — not just a hidden URL.

### What is the difference between NPS and star rating?

NPS (Net Promoter Score) uses a 0-10 scale and asks 'how likely are you to recommend this?' — respondents are grouped into promoters (9-10), passives (7-8), and detractors (0-6), and the score is the promoter percentage minus the detractor percentage. Star rating (1-5) measures satisfaction with a specific interaction. Use NPS for product-level health tracking and star rating for per-interaction quality scoring.

### How do I stop spam submissions from bots?

Two layers: a unique constraint at the database level on (user_id, created_at::date) prevents more than one authenticated submission per day. For anonymous submissions, add a honeypot hidden field that bots fill in but real users ignore — reject any submission where the honeypot field is not empty. Cloudflare Turnstile is the strongest option if spam is a serious concern.

### Can I send feedback to my email automatically?

Yes — a Supabase Edge Function triggered by an INSERT on the feedback table calls the Resend API to email you the rating, category, and body. Resend free tier covers 3,000 emails/month, which is more than sufficient unless you are sending a notification for every single submission from a high-traffic app. Filter to only critical ratings (NPS 0-6) to keep the volume manageable.

### How do I add conditional questions that only show based on previous answers?

Conditional logic (show question B only if answer A is below 7) requires storing the form schema as a JSONB object and rendering questions dynamically based on the previous answer. Lovable's first-pass generates a static form — you need to follow up with a prompt specifying the exact condition. For complex multi-branch surveys, a custom build or a dedicated tool like Typeform is more practical.

### Is there a free way to store feedback responses?

Yes. Supabase free tier stores up to 500 MB of data with unlimited rows — a feedback table row is roughly 1 KB, so the free tier holds hundreds of thousands of responses before any cost is incurred. You only need Supabase Pro ($25/mo) when you need production uptime guarantees, not because of storage volume.

---

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