Skip to main content
RapidDev - Software Development Agency
App Featurescommunication-social20 min read

How to Add User Polling with Anonymous Voting to Your App (Copy-Paste Prompts)

Anonymous polls need four pieces: a poll creation form, an anonymous voter token (UUID SHA-256 hashed before DB storage), a Supabase unique constraint that blocks duplicate votes without exposing identity, and a live results view via Supabase Realtime. With Lovable or V0, ship a working poll in 4-8 hours for $0/month on the free tier. AI tools often skip the hashing step — that is the one piece always worth reviewing after generation.

4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members

Feature spec

Intermediate

Category

communication-social

Build with AI

4-8 hours with Lovable or V0

Custom build

3-5 days custom dev

Running cost

$0/mo up to 1,000 users · $25-50/mo at 10K users

Works on

WebMobile

Everything it takes to ship User Polling with Anonymous Voting — parts, prompts, and real costs.

TL;DR

Anonymous polls need four pieces: a poll creation form, an anonymous voter token (UUID SHA-256 hashed before DB storage), a Supabase unique constraint that blocks duplicate votes without exposing identity, and a live results view via Supabase Realtime. With Lovable or V0, ship a working poll in 4-8 hours for $0/month on the free tier. AI tools often skip the hashing step — that is the one piece always worth reviewing after generation.

What an Anonymous Polling Feature Actually Requires

Polling with anonymous voting sounds simple but has one genuinely hard problem: how do you prevent the same person from voting twice without knowing who they are? The answer is a voter token — a UUID generated on the client, stored in localStorage (web) or AsyncStorage (mobile), and SHA-256 hashed before being saved to the database. The unique constraint on (poll_id, voter_token_hash) blocks duplicate votes at the database level without ever storing a raw identity. The rest of the feature — poll creation form, live results chart, expiry handling, and creator controls — is straightforward with AI tools and Supabase Realtime.

What users consider table stakes in 2026

  • Single-click vote with immediate live results update — the results bar chart should animate within 200ms of voting, not require a page reload
  • Clear 'Anonymous' label on the voting UI so users understand their choice is not tied to their account
  • Duplicate vote prevention without requiring login — users on the same device cannot vote twice, enforced visually and at the database level
  • Results displayed as percentage bars with the vote count per option visible on hover
  • Poll expiry countdown timer showing hours and minutes remaining; vote button disabled automatically when expired
  • Creator controls to close the poll early, view total vote count, and export results as CSV

Anatomy of the Feature

Six components. The poll form and live results generate reliably with AI tools. The anonymous vote token and dedup constraint are where first builds typically break — AI omits the hashing step by default.

Layers:UIBackend

Poll creation form

UI

shadcn/ui Form with react-hook-form managing fields: question (text), options (dynamic list with min 2 / max 10 inputs), expiry datetime picker, and an anonymous toggle. Zod validation schema enforces question max 280 characters, minimum 2 options, and expiry at least 5 minutes in the future.

Note: Store options as a JSONB array in the polls table rather than a separate options table — simpler schema and faster query for the results view.

Anonymous voter token

Backend

On a user's first visit to any poll, generate a UUID using crypto.randomUUID() and store it in localStorage (web) or AsyncStorage (React Native/FlutterFlow). Before inserting a vote, hash the raw token with the Web Crypto API (SHA-256) and store only the hash — never the raw UUID. This means the DB cannot be used to link votes back to a device even if the votes table is exposed.

Note: This is the component AI tools most frequently get wrong. Verify that the generated code calls crypto.subtle.digest('SHA-256', ...) before the Supabase insert, not after.

Vote recording

Backend

Supabase RLS policy allows INSERT on the votes table only when no existing row matches (poll_id, voter_token_hash) — enforced by a unique constraint at the database level. The policy also checks that the poll's expires_at is in the future and is_closed is false, preventing late votes without requiring client-side validation alone.

Note: The unique constraint (poll_id, voter_token_hash) is the core dedup mechanism. Without it, a race condition on rapid double-clicks can insert two votes even with a client-side guard.

Live results

Backend

Supabase Realtime subscription on the votes table (filtered by poll_id) triggers a React state update that recalculates vote counts per option and re-renders a Recharts BarChart or shadcn/ui Progress bars. The subscription uses Postgres Changes (not Broadcast) so late-joining viewers see current totals immediately on subscribe.

Note: Guard against division-by-zero: if totalVotes === 0 render 0% for all options. Initialize the chart with zeros on mount so Recharts never encounters NaN%.

Poll expiry guard

Backend

Client-side: a useEffect interval computes time remaining from poll.expires_at and disables the vote button when expired. Server-side: the Supabase RLS INSERT policy includes a check (polls.expires_at > now()) so the constraint holds even if a client bypasses the UI.

Note: For polls that need server-enforced closure at exactly the expiry time (not just on next user interaction), add a Supabase Edge Function on a pg_cron schedule that sets is_closed = true for expired polls.

Admin controls

UI

Creator-only dashboard showing total votes, breakdown per option, close-poll button, and a CSV export of vote counts (not voter identities). Protected by Supabase RLS checking that auth.uid() = polls.creator_id. CSV export runs a SELECT COUNT(*) GROUP BY option_index query via a Supabase RPC function.

Note: Never include voter_token_hash in the exported CSV — export only option labels and vote counts. The hash is for dedup only, not for analysis.

The data model

Two tables: polls stores the question and metadata, votes stores one hashed-token row per vote. Run this in the Supabase SQL Editor (Dashboard → SQL Editor):

schema.sql
1create table public.polls (
2 id uuid primary key default gen_random_uuid(),
3 creator_id uuid references auth.users(id) on delete cascade,
4 question text not null check (length(question) <= 280),
5 options jsonb not null,
6 expires_at timestamptz not null,
7 is_anonymous boolean not null default true,
8 is_closed boolean not null default false,
9 created_at timestamptz not null default now()
10);
11
12create table public.votes (
13 id uuid primary key default gen_random_uuid(),
14 poll_id uuid references public.polls(id) on delete cascade not null,
15 option_index integer not null,
16 voter_token_hash text not null,
17 created_at timestamptz not null default now(),
18 constraint votes_poll_voter_unique unique (poll_id, voter_token_hash)
19);
20
21alter table public.polls enable row level security;
22alter table public.votes enable row level security;
23
24create policy "Anyone can read active polls"
25 on public.polls for select
26 using (true);
27
28create policy "Authenticated users can create polls"
29 on public.polls for insert
30 with check (auth.uid() = creator_id);
31
32create policy "Creator can update own poll"
33 on public.polls for update
34 using (auth.uid() = creator_id)
35 with check (auth.uid() = creator_id);
36
37create policy "Creator can delete own poll"
38 on public.polls for delete
39 using (auth.uid() = creator_id);
40
41create policy "Anyone can insert a vote if poll is open"
42 on public.votes for insert
43 with check (
44 exists (
45 select 1 from public.polls
46 where id = poll_id
47 and is_closed = false
48 and expires_at > now()
49 )
50 );
51
52create policy "Anyone can read votes (for results)"
53 on public.votes for select
54 using (true);
55
56create policy "Creator can read own poll votes"
57 on public.votes for select
58 using (
59 exists (
60 select 1 from public.polls
61 where id = poll_id
62 and creator_id = auth.uid()
63 )
64 );
65
66create index votes_poll_id_idx
67 on public.votes (poll_id, option_index);
68
69create index polls_expires_at_idx
70 on public.polls (expires_at) where is_closed = false;

Heads up: The unique constraint votes_poll_voter_unique is the dedup backbone. The options column is a JSONB array of strings (e.g., ['Option A', 'Option B']) — option_index in votes references the array position, keeping the votes table simple and the query fast.

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.

Hand-built by developersFit for this feature:

Full control over fraud prevention, weighted voting, ranked-choice logic, and analytics integration. The right path when poll integrity is business-critical or when you need multi-choice and ranked voting variants.

Step by step

  1. 1Implement server-side voter fingerprinting combining hashed IP address + user-agent as a secondary dedup signal alongside the client token — raises the friction for motivated duplicate voters
  2. 2Add IP-based rate limiting (one vote per IP per poll per hour) using Supabase Edge Function with Upstash Redis for the rate limit counter
  3. 3Build ranked-choice tallying logic (Instant Runoff Voting) in a PostgreSQL stored procedure that runs on poll close
  4. 4Integrate poll results with your analytics pipeline (PostHog, Mixpanel) to track conversion from view to vote and option selection patterns

Where this path bites

  • Even custom implementations cannot fully prevent determined duplicate voters without requiring login — anonymous voting has an inherent tradeoff between privacy and integrity

Third-party services you'll need

Anonymous polls can run entirely on free-tier services. The only paid costs are Supabase Pro if you exceed the free concurrent Realtime connection limit, and Recharts which is free MIT-licensed:

ServiceWhat it doesFree tierPaid from
SupabaseDatabase (polls + votes tables), RLS for dedup enforcement, Realtime Postgres Changes for live resultsFree — 500 concurrent connections, 500MB databasePro $25/mo — 10,000 concurrent connections, 8GB database
RechartsClient-side BarChart and Pie chart for vote result visualizationFree (MIT license)Free
Web Crypto APISHA-256 hashing of voter tokens before database storage — prevents identity linkageBrowser-native, no install requiredFree

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

$0/mo

Supabase free tier covers polls and votes easily at this scale. Realtime Postgres Changes are within the 500 concurrent connection limit. Web Crypto hashing runs entirely in the browser at zero cost.

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.

Duplicate votes from incognito tabs or cleared localStorage

Symptom: The anonymous voter token lives in localStorage. Any user who opens an incognito window, clears browser storage, or uses a different device gets a fresh UUID that generates a new unique hash — allowing them to vote again. This is an inherent limitation of client-side anonymous tokens, not a bug.

Fix: For casual polls, this is acceptable — add a server-side secondary signal (hashed IP + user-agent) as a soft dedup without exposing it to users. For high-stakes polls (product decisions, elections), require login instead of anonymous tokens. Document clearly in your UI that anonymous votes have soft dedup, not hard identity-based prevention.

Recharts renders NaN% when the poll has zero votes

Symptom: When a poll has just been created and has zero votes, the percentage calculation (0 / 0 * 100) produces NaN. Recharts renders this as 'NaN%' in the bar label and can throw a console error. This happens every time a new poll loads before any votes are cast.

Fix: Add a guard before rendering: const pct = totalVotes === 0 ? 0 : (count / totalVotes) * 100. Initialize all option counts to 0 on component mount so the chart renders immediately with empty bars rather than waiting for the first Realtime event.

AI generates token storage without SHA-256 hashing

Symptom: Lovable and V0 both commonly generate the voter token as a raw UUID stored directly in the votes table. This means anyone with database access (including Supabase Dashboard users or a leaked service key) can correlate tokens with device fingerprints or browser storage to de-anonymize voters.

Fix: After generation, locate the vote submission handler and confirm the code calls crypto.subtle.digest('SHA-256', ...) before the Supabase insert. If missing, add it: hash the raw UUID with Web Crypto API and insert only the hex string of the hash.

Creator RLS blocks them from reading vote results

Symptom: AI-generated RLS on the votes table often restricts SELECT to anonymous voters (no auth) but forgets to add a creator exception. This means the poll creator's admin dashboard returns zero results even though votes exist.

Fix: Add an explicit RLS policy: SELECT allowed when auth.uid() = (SELECT creator_id FROM polls WHERE id = poll_id). This gives creators full read access to their poll's votes without exposing other polls' data.

FlutterFlow SharedPreferences voter token wiped on app update

Symptom: On some Android OEM builds (particularly Samsung and Huawei), SharedPreferences is cleared during an app update. The voter gets a new token on the next launch and can vote again on any previously voted poll.

Fix: Use flutter_secure_storage instead of SharedPreferences for the voter token — it stores the value in the OS Keychain (iOS) or Android Keystore, which persists across app updates and restores from backup.

Best practices

1

Always hash the voter token with SHA-256 before inserting into the database — store the hex hash, never the raw UUID, to protect voter anonymity even from database administrators

2

Enforce the unique constraint (poll_id, voter_token_hash) at the database level, not just in application code — a race condition on double-click can bypass client-side guards

3

Show the 'Anonymous' badge prominently on the voting UI so users understand and trust the system before they vote

4

Guard the Recharts chart against zero-vote division: initialize all option counts to 0 and guard (count / totalVotes * 100) with a totalVotes === 0 check

5

Set an explicit maximum expiry window (30 days) and close polls automatically with a Supabase scheduled function — orphaned polls with thousands of rows accumulate fast

6

Never expose voter_token_hash in the CSV export or any user-facing view — export only option labels and vote counts

7

Add client-side optimistic update when a user votes — immediately show 'You voted for X' and the estimated result without waiting for the Realtime subscription to confirm, then reconcile with the actual DB result

When You Need Custom Development

AI tools handle simple single-choice anonymous polls well. Step up to custom development when:

  • Weighted voting, ranked-choice (Instant Runoff Voting), or approval voting is required — these need custom tallying logic that AI tools don't scaffold correctly
  • Compliance requirement to fully prevent vote manipulation using IP + device + account triangulation — requires server-side rate limiting, fingerprinting infrastructure, and audit logs
  • Poll results need to flow into an existing analytics pipeline or CRM (PostHog, Mixpanel, HubSpot) in real time to drive downstream actions
  • Real-time poll moderation is required: flagging offensive options, removing answers mid-poll, or moderator controls on live event polling with thousands of simultaneous voters

RapidDev builds these features for production

Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.

Get a fixed-price quote

Frequently asked questions

How do I stop the same person from voting twice if they are anonymous?

You generate a UUID on first visit using crypto.randomUUID() and store it in localStorage. Before every vote, SHA-256 hash the UUID and enforce a unique constraint on (poll_id, voter_token_hash) in Supabase. This blocks same-device duplicate votes without revealing who the voter is. Someone who clears their storage or uses incognito can vote again — for high-stakes polls, require account login instead.

Can I show poll results before someone votes?

Yes — it's a UI design choice, not a technical constraint. Add a 'Show results before voting' toggle to the poll creation form stored as a column in the polls table. When true, render the results chart alongside the vote options. When false, show only the options and reveal results after the user votes or after the poll closes.

What is the difference between anonymous and private voting?

Anonymous voting means the system cannot link a vote to a specific person — even the app owner cannot tell who voted for what. Private voting means the vote is associated with an authenticated user but not shown to other users publicly. Anonymous is achieved with hashed tokens and no login requirement. Private voting requires authentication and RLS policies that restrict SELECT to the poll creator.

How do I export poll results to a spreadsheet?

Add a 'Download CSV' button in the creator dashboard that calls a Supabase RPC function: SELECT option_index, COUNT(*) as vote_count FROM votes WHERE poll_id = $1 GROUP BY option_index ORDER BY option_index. Join with the options JSONB array to get option labels. Convert to CSV in the browser using a simple array-to-CSV function and trigger a download with a Blob URL. Never include voter_token_hash in the export.

Can I embed a poll inside a blog post or landing page?

Yes — build the poll as an iframe-embeddable component. Create a dedicated /polls/{id}/embed route that renders the voting UI without your app's navigation. Set the Content-Security-Policy to allow framing from allowed domains. The Supabase Realtime subscription works inside iframes as long as the page is served over HTTPS.

How many options can a poll have?

The brief sets the practical limit at 10 options per poll — enforce this with a Zod validation constraint (z.array(z.string().min(1)).min(2).max(10)) on the creation form and a database CHECK constraint on the options JSONB column. Polls with more than 10 options have significantly lower completion rates; if you need more, consider a ranking or bracket format instead.

Is it possible to add image options to a poll?

Yes. Change the options JSONB schema from an array of strings to an array of objects: [{ label: 'Option A', image_url: 'https://...' }]. Upload images to Supabase Storage during poll creation and store the signed URL in the image_url field. Render each option with the image above the label text in the voting UI.

Can I schedule a poll to open and close automatically?

Yes. Add opens_at and expires_at columns to the polls table. The RLS INSERT policy checks opens_at <= now() AND expires_at > now(). Add a pg_cron job in Supabase to set is_closed = true for expired polls every minute. On the client, disable the vote button and show a countdown when the poll hasn't opened yet or when expires_at has passed.

RapidDev

Need this feature production-ready?

RapidDev builds user polling with anonymous voting into real apps — auth, database, payments — at $13K–$25K.

Book a free consultation

30-min call. No commitment.

Want this built for you?

We ship production apps at a fixed price — $13K–$25K, 6–10 weeks, source code yours. You've seen what it takes; we do it every week.

Get a fixed-price quote

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.