Skip to main content
RapidDev - Software Development Agency
flutterflow-tutorials

How to Create a Voting System in FlutterFlow

Create a polling system with a polls collection storing the question, options with vote counts, and an endTime, plus a votes subcollection for one-vote-per-user enforcement. Each option renders as a tappable Container with a percentage fill bar driven by voteCount divided by totalVotes. A Firestore transaction atomically creates the vote document and increments the count. Real-time queries keep percentages updating live as votes come in.

What you'll learn

  • How to model polls with options and vote counts in Firestore
  • How to enforce one-vote-per-user using a votes subcollection
  • How to build percentage fill bars that update in real time
  • How to use Firestore transactions for atomic vote counting
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner8 min read20-25 minFlutterFlow Free+March 2026RapidDev Engineering Team
TL;DR

Create a polling system with a polls collection storing the question, options with vote counts, and an endTime, plus a votes subcollection for one-vote-per-user enforcement. Each option renders as a tappable Container with a percentage fill bar driven by voteCount divided by totalVotes. A Firestore transaction atomically creates the vote document and increments the count. Real-time queries keep percentages updating live as votes come in.

Building a Real-Time Voting and Polling System in FlutterFlow

Polls and voting features are essential for community apps, feedback collection, and engagement. This tutorial builds a complete voting system with real-time percentage bars, duplicate vote prevention, atomic counting, and both active and expired poll states. It is aimed at founders building community, feedback, or decision-making features.

Prerequisites

  • A FlutterFlow project with Firebase Authentication enabled
  • Firestore database set up in your Firebase project
  • Basic understanding of FlutterFlow Backend Queries and Action Flows
  • Familiarity with Firestore subcollections

Step-by-step guide

1

Create the polls and votes Firestore schema

In Firestore, create a polls collection with fields: question (String), options (List of Maps, each with text and voteCount integer), totalVotes (Integer, default 0), endTime (Timestamp), createdBy (String), and isActive (Boolean). Under each poll document, create a votes subcollection with documents keyed by userId, containing optionIndex (Integer) and timestamp (Timestamp). The subcollection approach ensures you can quickly check whether a user has already voted with a single document read.

Expected result: Your Firestore has a polls collection with nested votes subcollections. Each poll document contains the question, options array, and totalVotes counter.

2

Build the poll display page with option cards and percentage bars

Create a PollDetail page that receives a pollId as a Page Parameter. Add a Backend Query for the poll document with Single Time Query set to OFF for real-time updates. Display the question in a Text widget with headlineMedium styling. Below it, add a ListView bound to the options array. For each option, create a Container with a Stack: the bottom layer is a colored Container whose width is set to (voteCount / totalVotes * parentWidth) via a Custom Function, creating a percentage fill bar. On top, add a Row with the option text and the vote count or percentage. Use Conditional Visibility to show vote counts only after the user has voted.

Expected result: Each poll option displays as a card with a colored percentage fill bar that grows proportionally to its vote share. The bars update in real time.

3

Implement the vote action with duplicate prevention

On each option Container, add an On Tap action. First, perform a Backend Query to check if a document exists at polls/{pollId}/votes/{currentUserId}. If it exists, show a SnackBar saying 'You have already voted'. If not, proceed with the vote. Also check if the poll's endTime is in the past or isActive is false, and show 'This poll has ended' if so. This prevents both duplicate votes and votes on expired polls before the write attempt.

Expected result: Tapping an option first checks for an existing vote and poll expiry. Users who have already voted or tap an expired poll see an informative message instead of recording a vote.

4

Write the vote atomically using a Cloud Function transaction

Create a Cloud Function called castVote that accepts pollId and optionIndex. Inside, run a Firestore transaction that reads the poll document, verifies the vote subcollection document does not exist for this user, increments the selected option's voteCount by 1, increments totalVotes by 1, and creates the vote document with the userId, optionIndex, and timestamp. The transaction ensures two simultaneous votes do not both succeed for the same user. In FlutterFlow, call this Cloud Function from the On Tap action after the duplicate check passes.

castVote.js
1// Cloud Function: castVote
2const functions = require('firebase-functions');
3const admin = require('firebase-admin');
4admin.initializeApp();
5
6exports.castVote = functions.https.onCall(async (data, context) => {
7 if (!context.auth) throw new functions.https.HttpsError('unauthenticated', 'Login required');
8 const { pollId, optionIndex } = data;
9 const uid = context.auth.uid;
10 const db = admin.firestore();
11 const pollRef = db.collection('polls').doc(pollId);
12 const voteRef = pollRef.collection('votes').doc(uid);
13
14 await db.runTransaction(async (tx) => {
15 const pollDoc = await tx.get(pollRef);
16 const voteDoc = await tx.get(voteRef);
17 if (voteDoc.exists) throw new functions.https.HttpsError('already-exists', 'Already voted');
18 if (!pollDoc.data().isActive) throw new functions.https.HttpsError('failed-precondition', 'Poll ended');
19
20 const options = pollDoc.data().options;
21 options[optionIndex].voteCount += 1;
22 tx.update(pollRef, { options, totalVotes: admin.firestore.FieldValue.increment(1) });
23 tx.set(voteRef, { optionIndex, timestamp: admin.firestore.FieldValue.serverTimestamp() });
24 });
25 return { success: true };
26});

Expected result: Votes are recorded atomically. The poll's option voteCount and totalVotes increment together, and duplicate votes are blocked at the transaction level.

5

Add poll creation form and expiry countdown

Create a CreatePoll page with a TextField for the question, a dynamic list of option TextFields (start with 2, Add Option button appends to Page State list, minimum 2 maximum 6), and a DateTimePicker for the poll end time. On submit, create the poll document with each option having voteCount: 0 and the selected endTime. Back on the PollDetail page, add a countdown Text widget that displays time remaining using a Custom Function calculating the difference between endTime and now. When the countdown reaches zero, update isActive to false via a Cloud Function scheduled trigger.

Expected result: Users can create new polls with a question, 2-6 options, and an expiry time. Active polls show a live countdown to closing.

6

Build the polls list page with status badges

Create a PollsList page with a Backend Query on a ListView that fetches all polls ordered by endTime descending. Add ChoiceChips at the top to filter by Active and Ended polls. Each poll card shows the question, total vote count, a status badge (green for active, red for ended using Conditional Styling), and the time remaining or 'Ended' label. Tap navigates to the PollDetail page passing the pollId. Add a FloatingActionButton that navigates to CreatePoll.

Expected result: The polls list page shows all polls with status badges and vote counts. Users can filter between active and ended polls and navigate to vote or view results.

Complete working example

castVote.js
1// Cloud Function: castVote — atomic vote with duplicate prevention
2const functions = require('firebase-functions');
3const admin = require('firebase-admin');
4admin.initializeApp();
5
6exports.castVote = functions.https.onCall(async (data, context) => {
7 if (!context.auth) {
8 throw new functions.https.HttpsError('unauthenticated', 'Must be logged in');
9 }
10
11 const { pollId, optionIndex } = data;
12 const uid = context.auth.uid;
13 const db = admin.firestore();
14 const pollRef = db.collection('polls').doc(pollId);
15 const voteRef = pollRef.collection('votes').doc(uid);
16
17 return db.runTransaction(async (tx) => {
18 const pollDoc = await tx.get(pollRef);
19 if (!pollDoc.exists) {
20 throw new functions.https.HttpsError('not-found', 'Poll not found');
21 }
22
23 const pollData = pollDoc.data();
24 if (!pollData.isActive) {
25 throw new functions.https.HttpsError('failed-precondition', 'Poll has ended');
26 }
27
28 const now = admin.firestore.Timestamp.now();
29 if (pollData.endTime.toMillis() < now.toMillis()) {
30 throw new functions.https.HttpsError('failed-precondition', 'Poll has expired');
31 }
32
33 const voteDoc = await tx.get(voteRef);
34 if (voteDoc.exists) {
35 throw new functions.https.HttpsError('already-exists', 'You have already voted');
36 }
37
38 if (optionIndex < 0 || optionIndex >= pollData.options.length) {
39 throw new functions.https.HttpsError('invalid-argument', 'Invalid option');
40 }
41
42 const options = [...pollData.options];
43 options[optionIndex] = {
44 ...options[optionIndex],
45 voteCount: (options[optionIndex].voteCount || 0) + 1,
46 };
47
48 tx.update(pollRef, {
49 options: options,
50 totalVotes: admin.firestore.FieldValue.increment(1),
51 });
52
53 tx.set(voteRef, {
54 optionIndex: optionIndex,
55 timestamp: admin.firestore.FieldValue.serverTimestamp(),
56 });
57
58 return { success: true, message: 'Vote recorded' };
59 });
60});
61
62// Scheduled: close expired polls
63exports.closeExpiredPolls = functions.pubsub
64 .schedule('every 5 minutes')
65 .onRun(async () => {
66 const now = admin.firestore.Timestamp.now();
67 const expired = await admin.firestore()
68 .collection('polls')
69 .where('isActive', '==', true)
70 .where('endTime', '<=', now)
71 .get();
72 const batch = admin.firestore().batch();
73 expired.docs.forEach((doc) => batch.update(doc.ref, { isActive: false }));
74 await batch.commit();
75 });

Common mistakes when creating a Voting System in FlutterFlow

Why it's a problem: Using FieldValue.increment on nested array option objects

How to avoid: Read the full options array in a transaction, modify the specific element's voteCount in code, and write the entire array back to the document.

Why it's a problem: Checking for duplicate votes on the client only without server enforcement

How to avoid: Use a Cloud Function with a Firestore transaction that atomically checks for an existing vote document and creates one if absent.

Why it's a problem: Not using real-time queries for the poll results display

How to avoid: Set Single Time Query to OFF on the poll document Backend Query so the percentage bars update live as new votes come in.

Best practices

  • Use the userId as the votes subcollection document ID for O(1) duplicate checks
  • Run a scheduled Cloud Function every few minutes to close polls whose endTime has passed
  • Show results only after the user votes to prevent bandwagon bias on active polls
  • Display both raw vote counts and percentages so users understand the scale
  • Add an animation to the percentage fill bar using a Container with an Animated width for a polished feel
  • Limit options to 2-6 per poll to keep the UI readable on mobile screens
  • Store poll creator's userId and only show edit/delete actions to the creator

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I need to build a voting system in FlutterFlow with Firestore. Show me the data schema for polls with options and vote counts, a votes subcollection for one-vote-per-user enforcement, and a Cloud Function that atomically records a vote using a Firestore transaction.

FlutterFlow Prompt

Create a poll display page with a question heading, option cards showing percentage fill bars that update in real time, and a vote action that prevents duplicate votes using a Firestore subcollection check.

Frequently asked questions

Can users change their vote after submitting?

Not by default. To allow vote changes, update the Cloud Function to check if a vote exists, decrement the old option's count, increment the new option's count, and update the vote document — all within a single transaction.

How do I show poll results as a pie chart instead of percentage bars?

Create a Custom Widget using the fl_chart package with a PieChart that maps each option's voteCount to a PieChartSectionData with proportional values and distinct colors.

Can I restrict voting to specific user roles or groups?

Yes. Add an allowedRoles or allowedGroupIds array to the poll document and check it in your Cloud Function before processing the vote. In the UI, use Conditional Visibility to hide the vote buttons for unauthorized users.

How do I handle polls with hundreds of thousands of votes?

For high-volume polls, use Firestore distributed counters. Instead of a single voteCount field, create a shards subcollection with multiple counter documents and aggregate them when displaying results.

Is it possible to make polls anonymous?

You can omit the userId from the displayed results while still storing it in the votes subcollection for duplicate prevention. The vote is anonymous to other users but traceable by the system.

Can RapidDev help build a production polling platform?

Yes. RapidDev can implement advanced polling features including weighted voting, ranked choice, real-time analytics dashboards, and integration with notification systems for poll results.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

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.