TL;DR
The one-paragraph version before you dive in.
This prompt kit builds a Typeform-style poll and survey tool where creators define questions (multiple choice, scale, short text), share a public link, and view live aggregate results. Anonymous respondents submit without signing up. The entire build runs in under 60 credits — it fits Lovable Free. The single gotcha is RLS: the responses table must have anonymous INSERT but zero SELECT for anon, or anyone can read how your users voted.
Setup checklist
Complete these steps in Lovable before pasting the starter prompt — takes about 5 minutes.
Cloud tab settings
Database
Stores surveys, questions, responses, and answers. The migration creates the critical RLS policies: anonymous INSERT on responses and answers, with zero SELECT for anon — the entire privacy story of the app.
- 1Click the + icon next to the preview panel, then click 'Cloud tab' → 'Database'.
- 2Lovable Cloud provisions Supabase Postgres automatically.
- 3The starter prompt will generate the migration. If it does not auto-run, paste the migration SQL into Cloud → Database → SQL Editor manually.
- 4After migration, verify the anon INSERT policy in SQL Editor: `SELECT tablename, policyname, cmd FROM pg_policies WHERE tablename IN ('responses', 'answers');` — you should see INSERT for anon, no SELECT for anon.
Auth
Email/password sign-in for survey creators. Respondents submit without authentication (anon role). require_auth=true surveys optionally gate the response form to signed-in users only.
- 1In Cloud tab → 'Users & Auth', confirm Email provider is enabled.
- 2No additional configuration needed for the anonymous respondent flow — Supabase anon key handles it.
- 3After the starter runs, sign up as the first creator and you will be directed to /surveys.
Edge Functions
The submit-response edge function validates required questions are answered, enforces one-response-per-user via fingerprint, and inserts the response + all answers in a single transaction — making partial submissions impossible.
- 1After the starter prompt runs, open Cloud tab → 'Edge Functions' to confirm submit-response is deployed.
- 2If it is missing, paste the atomic submit-response follow-up prompt.
- 3Edge functions in Lovable Cloud run on Deno and have access to Cloud → Secrets via Deno.env.get().
Preflight checklist
- You are in a fresh Lovable project (Vite + React + TypeScript + Tailwind + shadcn/ui already scaffolded).
- Lovable Free tier is enough for the full build — the prompt chain runs ~30-60 credits, well within the monthly cap.
- Decide before pasting: do you need real-time results (live audience poll) or just on-demand results? Real-time needs Supabase Realtime enabled, which is a separate follow-up prompt.
- The RLS pattern here — anon INSERT / owner SELECT — is the same as a feedback form but applied to structured question data. If Lovable's first scaffold gets it wrong (grants SELECT to anon), the verification follow-up prompt fixes it in minutes.
The starter prompt
Copy this. Paste it into Lovable Build mode (the default chat at the bottom-left of the editor). Hit send.
Build a Typeform-style poll and survey tool using Vite + React + TypeScript + Tailwind CSS + shadcn/ui, backed by Supabase (Lovable Cloud). Here is the exact schema, RLS, routes, and components to scaffold:
**Database migration (0001_surveys_schema.sql):**
Create four tables:
1. `surveys` — id uuid pk default gen_random_uuid(), owner_id uuid references auth.users not null, slug text unique not null, title text not null, description text, status text check (status in ('draft','published','closed')) default 'draft', allow_multiple_submissions bool default false, require_auth bool default false, created_at timestamptz default now(), closes_at timestamptz nullable. RLS: public-read where status='published', per-owner write via auth.uid()=owner_id.
2. `questions` — id uuid pk default gen_random_uuid(), survey_id uuid references surveys not null, position int not null, type text check (type in ('single_choice','multiple_choice','scale_1_5','scale_1_10','short_text','long_text')) not null, question_text text not null, options jsonb (array of strings for choice questions, null for text/scale), required bool default true. RLS: public-read where the parent survey is published (use a SELECT policy checking EXISTS(SELECT 1 FROM surveys WHERE id = survey_id AND status = 'published')), per-owner write.
3. `responses` — id uuid pk default gen_random_uuid(), survey_id uuid references surveys not null, respondent_user_id uuid nullable references auth.users, respondent_fingerprint text not null, submitted_at timestamptz default now(), ip_hash text. RLS: **anon and authenticated INSERT WITH CHECK (EXISTS (SELECT 1 FROM surveys WHERE id = survey_id AND status = 'published'))**, NO SELECT for anon whatsoever, owner SELECT via inner join: `EXISTS (SELECT 1 FROM surveys WHERE surveys.id = responses.survey_id AND surveys.owner_id = auth.uid())`. Add unique constraint on (survey_id, respondent_fingerprint) — enforced by application logic, not DB constraint, because fingerprints are soft-checked in the edge function when allow_multiple_submissions=false.
4. `answers` — id uuid pk default gen_random_uuid(), response_id uuid references responses not null, question_id uuid references questions not null, value_text text nullable, value_int int nullable, value_choice text nullable, value_choices text[] nullable. RLS: same anon INSERT pattern as responses (WITH CHECK via response → survey published), owner SELECT via response → survey ownership.
**Layouts:**
- `AppLayout.tsx` — sidebar for creators: My Surveys / New Survey / Templates. Wrap in CreatorGuard (checks auth session).
- `PublicLayout.tsx` — minimal respondent layout: logo + survey title + progress bar. No nav, no distractions.
**Pages and routes:**
- `/surveys` — creator list of all their surveys with status chips and response counts.
- `/surveys/new` — redirect to /surveys/{new-id}/edit after creating a blank draft survey.
- `/surveys/[id]/edit` — survey editor: title + description at top, question list below. Each question shows its type, text, and options. Drag-to-reorder via a drag handle (use @dnd-kit/core).
- `/surveys/[id]/results` — results dashboard: for single/multiple choice questions, show shadcn horizontal bar chart (Recharts BarChart) with percentage + count. For scale questions, show average + histogram. For text questions, show the last 20 responses as a scrollable list.
- `/s/[slug]` — public respond page: shows one question at a time on mobile (progress bar at top). Use shadcn RadioGroup for single_choice, Checkbox group for multiple_choice, Slider for scale questions, Textarea for text. Next/Previous buttons. Submit at the end.
- `/s/[slug]/thanks` — thank you page shown after submission. If the survey is closed (closes_at < now()), show 'This survey is closed' instead.
- `/signin` — Supabase Auth form.
**Key components:**
- `QuestionEditor.tsx` — renders an editable question card: type selector (shadcn Select), question text input, options editor for choice questions (add/remove option rows), required toggle.
- `QuestionRenderer.tsx` — renders one question for the respondent based on type. Cases: single_choice → RadioGroup, multiple_choice → Checkbox group, scale_1_5 → row of 5 numbered buttons, scale_1_10 → row of 10 numbered buttons, short_text → Input, long_text → Textarea.
- `ResultsChart.tsx` — accepts question + aggregated data + renders the appropriate visualization per question type.
- `ProgressBar.tsx` — shows current question index / total questions as a thin colored bar at the top of the respond page.
- `SurveyShareDialog.tsx` — opens on 'Share' button, shows the public URL and a copy-to-clipboard button.
- `CreatorGuard.tsx` — checks Supabase auth session, redirects to /signin if not authenticated.
**Submit flow:** the submit button on the last question calls the `submit-response` edge function via fetch, passing { survey_id, fingerprint, answers: [{question_id, value_text/value_int/value_choice/value_choices}] }. Fingerprint = auth.uid() if signed in, or localStorage UUID + IP hash for anon.
**Styling:** friendly Typeform-vibe — large rounded buttons (rounded-2xl), one question visible at a time on mobile, slate-50 background, indigo-600 primary; shadcn Card, RadioGroup, Slider, Textarea, Progress.
**Deliverables:** schema with anon-INSERT-only RLS on responses + answers, creator dashboard with survey CRUD, drag-reorder question editor, public one-question-per-screen respond flow, results dashboard with charts, and a basic submit-response edge function stub.What this prompt generates
- Generates the full migration with 4 tables and the critical anon-INSERT-only RLS policies on responses and answers.
- Scaffolds AppLayout and PublicLayout for creator vs. respondent contexts.
- Creates the creator survey list, question editor with drag-reorder, and share dialog.
- Builds the public one-question-per-screen respond flow with progress bar.
- Creates the results dashboard with Recharts bar charts for choice questions and scale histograms.
- Wires the submit-response edge function stub for atomic insertion.
Paste into: Lovable Build mode (the default chat at the bottom-left of the editor)
Expected output
What Lovable will generate after the starter prompt runs successfully.
Files
src/layouts/AppLayout.tsxCreator sidebar layout with CreatorGuard
src/layouts/PublicLayout.tsxMinimal respondent layout with progress bar
src/pages/Surveys.tsxCreator list of surveys with status + response counts
src/pages/SurveyEditor.tsxQuestion builder with drag-reorder and type selector
src/pages/SurveyResults.tsxResults dashboard — charts per question type
src/pages/Respond.tsxPublic one-question-per-screen respond flow
src/pages/ThankYou.tsxPost-submission confirmation page
src/components/QuestionEditor.tsxEditable question card with type/options
src/components/QuestionRenderer.tsxRenders correct input for each question type
src/components/ResultsChart.tsxRecharts visualization per question type
src/components/ProgressBar.tsxThin progress indicator at top of respond page
src/components/SurveyShareDialog.tsxPublic URL share + copy-to-clipboard
supabase/functions/submit-response/index.tsValidates + deduplicates + inserts response + answers atomically
supabase/migrations/0001_surveys_schema.sql4 tables + RLS + indexes
Routes
Creator's survey list
Create new survey (redirects to editor)
Survey question builder
Results dashboard with charts
Public respond flow
Post-submission thank you page
Creator sign-in form
DB Tables
surveys| Column | Type |
|---|---|
| id | uuid primary key |
| owner_id | uuid references auth.users not null |
| slug | text unique not null |
| title | text not null |
| status | text check in ('draft','published','closed') |
| allow_multiple_submissions | bool default false |
| require_auth | bool default false |
| closes_at | timestamptz nullable |
RLS: Public-read where status='published'. Per-owner write via owner_id=auth.uid().
questions| Column | Type |
|---|---|
| id | uuid primary key |
| survey_id | uuid references surveys |
| position | int not null |
| type | text check in ('single_choice','multiple_choice','scale_1_5','scale_1_10','short_text','long_text') |
| question_text | text not null |
| options | jsonb (array for choice questions, null for others) |
| required | bool default true |
RLS: Public-read where parent survey is published. Per-owner write.
responses| Column | Type |
|---|---|
| id | uuid primary key |
| survey_id | uuid references surveys |
| respondent_user_id | uuid nullable references auth.users |
| respondent_fingerprint | text not null |
| submitted_at | timestamptz default now() |
| ip_hash | text |
RLS: Anon + authenticated INSERT WITH CHECK survey is published. NO SELECT for anon. Owner SELECT via inner join to surveys.
answers| Column | Type |
|---|---|
| id | uuid primary key |
| response_id | uuid references responses |
| question_id | uuid references questions |
| value_text | text nullable |
| value_int | int nullable |
| value_choice | text nullable |
| value_choices | text[] nullable |
RLS: Same anon INSERT pattern as responses. Owner SELECT via response → survey ownership.
Components
QuestionEditorEditable question card with type selector and options editor
QuestionRendererRenders the correct input widget for each question type
ResultsChartRecharts visualization per question type in results dashboard
ProgressBarThin progress indicator for the respond flow
CreatorGuardRedirects to /signin if auth session is missing
Follow-up prompts
Paste these into Agent Mode one by one, in order, after the starter prompt finishes.
Anonymous-response RLS audit and lockdown
Verifies anon has INSERT-only access to responses and answers, with no SELECT leakage
Run the following verification in Cloud → Database → SQL Editor to confirm the RLS policies on responses and answers are correct:
```sql
SELECT tablename, policyname, cmd, roles
FROM pg_policies
WHERE tablename IN ('responses', 'answers')
ORDER BY tablename, cmd;
```
You should see:
- responses: INSERT for {anon, authenticated}, SELECT for authenticated only
- answers: INSERT for {anon, authenticated}, SELECT for authenticated only
If SELECT for anon appears on responses or answers, run:
```sql
DROP POLICY IF EXISTS responses_anon_select ON responses;
DROP POLICY IF EXISTS answers_anon_select ON answers;
```
Then verify in an incognito browser using the browser console:
```javascript
const { data, error } = await supabase.from('responses').select();
console.log(data, error);
```
The result must be 0 rows returned (or a Postgres RLS error). If data contains rows, the SELECT policy for anon is still active. Drop it and re-verify. This test must pass before any public launch — anyone with your Supabase project URL can otherwise read all survey responses.When to use: Paste immediately after the starter — the #1 gotcha in every survey/poll build
Atomic submit-response edge function
Validates required questions, enforces one-response-per-fingerprint, and inserts atomically via edge function
Upgrade the submit-response edge function to validate required questions and insert atomically:
In supabase/functions/submit-response/index.ts:
```typescript
import { createClient } from 'jsr:@supabase/supabase-js@2';
Deno.serve(async (req) => {
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization,content-type',
};
if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders });
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,
);
const { survey_id, fingerprint, answers } = await req.json();
// Validate survey is published
const { data: survey } = await supabase.from('surveys').select('id,allow_multiple_submissions,require_auth,closes_at').eq('id', survey_id).eq('status', 'published').maybeSingle();
if (!survey) return new Response(JSON.stringify({ ok: false, reason: 'survey_not_found' }), { status: 404, headers: corsHeaders });
if (survey.closes_at && new Date(survey.closes_at) < new Date()) return new Response(JSON.stringify({ ok: false, reason: 'survey_closed' }), { status: 410, headers: corsHeaders });
// Enforce one-response-per-fingerprint when allow_multiple_submissions=false
if (!survey.allow_multiple_submissions) {
const { count } = await supabase.from('responses').select('*', { count: 'exact', head: true }).eq('survey_id', survey_id).eq('respondent_fingerprint', fingerprint);
if ((count ?? 0) > 0) return new Response(JSON.stringify({ ok: false, reason: 'already_submitted' }), { status: 409, headers: corsHeaders });
}
// Validate required questions are answered
const { data: questions } = await supabase.from('questions').select('id,required,type').eq('survey_id', survey_id);
const answerMap = new Map(answers.map((a: any) => [a.question_id, a]));
for (const q of (questions ?? [])) {
if (!q.required) continue;
const a = answerMap.get(q.id);
const hasValue = a && (a.value_text || a.value_int != null || a.value_choice || (a.value_choices && a.value_choices.length > 0));
if (!hasValue) return new Response(JSON.stringify({ ok: false, reason: 'missing_required', question_id: q.id }), { status: 422, headers: corsHeaders });
}
// Insert response + answers (not truly atomic without a transaction, but service-role insert is fast)
const { data: response } = await supabase.from('responses').insert({ survey_id, respondent_fingerprint: fingerprint }).select('id').single();
await supabase.from('answers').insert(answers.map((a: any) => ({ ...a, response_id: response.id })));
return new Response(JSON.stringify({ ok: true, response_id: response.id }), { headers: corsHeaders });
});
```
This function uses the service-role key so it bypasses RLS and can insert on behalf of anon users. The RLS on the client-side tables still applies to any direct Supabase JS calls from the browser.When to use: After the RLS audit — once you have multi-question surveys, partial submission prevention matters
One-response-per-user enforcement via fingerprint
One-response-per-fingerprint check on the respond page before the form renders
Strengthen the one-submission enforcement in Respond.tsx:
1. For signed-in users: fingerprint = auth.uid(). This is the strongest deduplication.
2. For anonymous users: generate a fingerprint as `localStorage.getItem('survey_fp') || crypto.randomUUID()`. Store it: `localStorage.setItem('survey_fp', fp)`. Include IP hash (from the edge function reading the x-forwarded-for header, hashed with SHA-256) as a second signal.
3. Before rendering the survey form, call the survey endpoint to check if this fingerprint has already submitted:
```typescript
const { count } = await supabase.from('responses').select('*', { count: 'exact', head: true }).eq('survey_id', surveyId).eq('respondent_fingerprint', fingerprint);
if (count > 0) navigate('/s/' + slug + '/thanks?already=1');
```
4. On the ThankYou page, if `?already=1` is present, show: 'You have already completed this survey.'
Note: cookie-clear + new IP bypasses this for anonymous users. For high-stakes surveys, set surveys.require_auth=true and enforce via auth.uid() — only authenticated users can submit, making deduplication cryptographically strong.When to use: After the atomic submit-response prompt — prevents ballot stuffing on public-facing polls
Real-time results dashboard for live audience polls
Supabase Realtime subscription makes the results dashboard update live as responses come in
Enable Supabase Realtime on the responses table so the SurveyResults page updates as new responses arrive — useful for live polls at events or webinars.
1. In Cloud → Database → Realtime, enable Realtime for the 'responses' table (toggle to ON).
2. In SurveyResults.tsx, subscribe to new responses for this survey:
```typescript
useEffect(() => {
const channel = supabase
.channel('responses-' + surveyId)
.on('postgres_changes', {
event: 'INSERT',
schema: 'public',
table: 'responses',
filter: `survey_id=eq.${surveyId}`,
}, () => {
// Re-fetch aggregations
refetchResults();
})
.subscribe();
return () => supabase.removeChannel(channel);
}, [surveyId]);
```
3. Show a live response counter at the top of the results page: 'X responses (live)'. The counter increments in real time.
4. Add a 'Live mode' toggle that hides individual text answers (to prevent reading individual submissions as they come in) and only shows aggregate counts.
Note: Realtime respects RLS — the subscription only fires for the survey owner because the SELECT policy on responses requires owner_id=auth.uid().When to use: Only if you run live audience polls at events — for async surveys, on-demand results are fine
AI question generation from a topic
One-click AI generation of 5 questions from a topic description using Lovable AI panel
Add an 'AI Suggest Questions' button to the survey editor that generates 5 questions from a topic.
1. Add a 'Suggest questions...' input at the top of the question editor: a text field for the survey topic and a 'Generate 5 questions' button.
2. On click, call the Lovable AI connector (Gemini 3 Flash, the default in the Lovable AI panel) with: 'Create 5 survey questions for the topic: "[topic]". Mix of question types: 2 single_choice, 1 scale_1_5, 1 short_text, 1 multiple_choice. Return JSON array: [{type, question_text, options (for choice questions), required: true}]'
3. Parse the response and insert all 5 questions into the questions table for this survey, positioned after existing questions.
4. Show a loading spinner during the call and a toast 'Added 5 questions — review and edit them.'
5. Handle errors: if the AI call fails or returns invalid JSON, show 'AI suggestion failed. Try rephrasing your topic.'
Do not auto-publish the survey after generating questions — creator must review and click Publish manually.When to use: Optional — pure creator UX improvement; skip if you prefer writing all questions manually
Common errors
Real error strings you'll see. Find yours, paste the fix prompt.
new row violates row-level security policy for table "responses"Default scaffold granted INSERT only to authenticated users — but respondents submit without accounts, using the Supabase anon role.
Add explicit anon INSERT policy in SQL Editor: `CREATE POLICY anon_submit ON responses FOR INSERT TO anon WITH CHECK (EXISTS (SELECT 1 FROM surveys WHERE id = survey_id AND status = 'published'));` Same shape for answers: `CREATE POLICY anon_answer ON answers FOR INSERT TO anon WITH CHECK (EXISTS (SELECT 1 FROM responses r JOIN surveys s ON s.id = r.survey_id WHERE r.id = response_id AND s.status = 'published'));` Critically: add NO SELECT policy for anon on either table.
anyone can read every survey response via /rest/v1/responsesLovable scaffolded a permissive SELECT policy for anon at the same time it added the INSERT policy — this exposes every respondent's answers to anyone with the Supabase project URL.
Drop all SELECT policies for anon: `DROP POLICY IF EXISTS responses_anon_select ON responses; DROP POLICY IF EXISTS answers_anon_select ON answers;` Add owner-only SELECT: `CREATE POLICY responses_owner_select ON responses FOR SELECT TO authenticated USING (EXISTS (SELECT 1 FROM surveys WHERE surveys.id = responses.survey_id AND surveys.owner_id = auth.uid()));` Verify in incognito: `supabase.from('responses').select()` must return 0 rows.duplicate key value violates unique constraint (or submit-response returns already_submitted)The same respondent fingerprint submitted twice to a survey with allow_multiple_submissions=false. This is the one-response-per-user enforcement working as designed.
In Respond.tsx, catch the `already_submitted` response from the edge function and navigate to /s/[slug]/thanks?already=1 instead of showing an error. On ThankYou.tsx, check the query param and show 'You have already completed this survey.' This is expected behavior — the user double-submitted.
partial response submitted — required questions skippedValidation was only in the React form; a user with JavaScript disabled or a direct POST to the edge function bypassed the client-side required checks.
Move required-question validation into submit-response edge function: for each question where required=true, verify an answer exists with a non-null value matching the question type. Return HTTP 422 with { reason: 'missing_required', question_id } if any required question is unanswered. The client-side validation is UX-only; the edge function is the source of truth.scale 1-5 question renders as a text inputQuestionRenderer does not have a case for type='scale_1_5' or type='scale_1_10' and falls through to the default text input.
In QuestionRenderer.tsx, add cases for scale_1_5 and scale_1_10 that render a row of numbered buttons (1 through 5 or 1 through 10) with onClick setting the value_int. Use shadcn ToggleGroup or a custom row of Button components. Example: `<div className='flex gap-2'>{Array.from({length: 5}, (_, i) => <Button key={i+1} variant={value === i+1 ? 'default' : 'outline'} onClick={() => setValue(i+1)}>{i+1}</Button>)}</div>`Cost reality
What this build actually costs — no surprises on your card.
Recommended Lovable plan
Free is enough — the entire build runs ~30-60 credits, well within the Free tier's ~30 monthly credits if you spread the prompts across 2 days. Pro $25/mo only if you want to run the full chain in a single session.
Monthly run cost breakdown
~30-60 credits: starter ~25, RLS audit ~15, atomic submit-response ~30, fingerprint dedupe ~20; real-time adds ~30 if needed; AI question generation adds ~30 more total| Item | Cost |
|---|---|
| Lovable Stop iterating after build is done | Free $0 |
| Supabase Surveys + responses stay tiny for years; 500K responses with 10 answers each is well under 500MB | Free $0 |
| Resend Only if you add result-threshold notifications | Free $0 (up to 3K emails/mo) |
| Custom domain Optional — works on lovable.app subdomain | ~$12/yr |
Scaling notes: Supabase Free 500MB DB handles ~500K responses with 10 answers each. The only real bottleneck is rate-limit-style abuse on anon INSERT — add IP rate limiting (3 submissions per hour per IP) in the edge function if a single high-traffic survey gets more than 10K responses per hour.
Production checklist
Steps to take before you share the URL with real users.
Domain & SSL
Connect your custom domain
Click Publish (top-right) → Settings → Custom Domain → enter your domain → follow the DNS CNAME or A record instructions. SSL is automatic.
RLS Verification
Verify anon INSERT / no SELECT on responses and answers before launch
Open an incognito browser → browser console → run `supabase.from('responses').select()`. Must return 0 rows or an RLS error. If it returns data, run the RLS audit follow-up prompt immediately.
Survey Hygiene
Set closes_at on any time-limited survey
In the survey editor, set closes_at to your intended end date. The respond page checks closes_at and shows 'This survey is closed' if past. The submit-response edge function also enforces it server-side.
Monitoring
Monitor for unexpected response spikes (bot flooding)
In Cloud → Database → SQL Editor, run periodically: `SELECT survey_id, count(*), max(submitted_at) FROM responses GROUP BY survey_id ORDER BY count DESC;` A spike of thousands of responses in minutes indicates bot flooding — add IP rate limiting in the submit-response edge function.
Frequently asked questions
Can respondents submit without signing up?
Yes. The default configuration allows anonymous submission. Respondents are identified by a fingerprint (their user ID if signed in, or a localStorage UUID for anonymous users) for deduplication, but they never need to create an account. If you want to require sign-in (for higher-confidence deduplication or compliance reasons), set surveys.require_auth=true on a per-survey basis — the respond page will then gate the form behind a Supabase Auth session.
Why are my survey responses visible to anyone who finds the API URL?
This is a misconfigured RLS policy on the responses table. When Lovable scaffolds anonymous INSERT access, it sometimes grants SELECT to anon at the same time. Anyone who knows your Supabase project URL can then call /rest/v1/responses and read all responses — including the text answers. The fix is to drop any SELECT policy for anon and replace it with an owner-only SELECT that requires the requesting user to own the survey. Run the anonymous-insert RLS audit follow-up prompt and test in an incognito browser before any public launch.
How do I prevent the same person from voting twice?
The submit-response edge function checks for an existing response with the same (survey_id, respondent_fingerprint) pair when allow_multiple_submissions=false. For signed-in users the fingerprint is auth.uid() — essentially impossible to spoof. For anonymous users the fingerprint is a localStorage UUID combined with an IP hash — clearing cookies or using a new IP defeats it. For high-stakes polls (votes, NPS surveys with real consequences), set surveys.require_auth=true so only authenticated users can submit, making the deduplication cryptographically strong.
Can I do branching or conditional logic (skip question 5 if question 2 was 'No')?
Yes, but expect to use 50+ credits for this follow-up. The implementation adds a depends_on jsonb column to questions (format: { question_id, operator, value }) and a skip-condition evaluator in QuestionRenderer that checks the current response state and skips questions whose conditions are not met. Lovable can scaffold the basic case, but complex branching trees (multiple conditions, loop-back logic) are where the agent tends to struggle and may need hand-finishing in Dev Mode.
How do real-time results work for live audience polls?
Enable Supabase Realtime on the responses table (Cloud → Database → Realtime → toggle ON for responses). In SurveyResults.tsx, subscribe to INSERT events filtered by survey_id. When a new response arrives, refetch the aggregations and update the charts. This creates a live-updating results page suitable for conference polls, webinar Q&As, or classroom quizzes. The subscription respects RLS — only the survey owner receives the real-time events.
Should I just use Tally instead?
For most use cases, yes. Tally is free, unlimited, and ships a beautiful survey in 2 minutes — Lovable cannot match that speed for a standalone survey tool. Build this in Lovable when: (1) you want surveys embedded inside an existing Lovable app with seamless authentication, (2) you need responses stored in your own Postgres for compliance reasons (HIPAA, GDPR data residency), or (3) the survey engine is the product itself (you are building a Typeform alternative). For a standalone NPS survey or onboarding quiz, just use Tally.
How big a survey can I run before Supabase free tier breaks?
Very large. Supabase Free tier includes 500MB of database storage. A response row is roughly 200 bytes; an answer row with a text value is roughly 500 bytes. With 10 questions per survey, one response ≈ 5KB. At that rate, 500MB holds approximately 100,000 responses. For most indie and small-business use cases, you will never hit this limit on a single survey. If you expect hundreds of thousands of responses (viral poll, company-wide survey), upgrade to Supabase Pro at $25/mo for 8GB storage and point-in-time backups.
If your survey tool needs branching logic, compliance exports, or CRM integration, what does RapidDev offer?
If your build outgrows this prompt kit and you need custom architecture, RapidDev builds production-grade Lovable apps at $13K-$25K — book a free 30-minute consultation at rapidevelopers.com.
Need a production-grade version?
RapidDev builds production-grade Lovable apps at $13K–$25K. Book a free 30-minute consultation.
Book a free consultation30-min call. No commitment.