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
Create the polls and votes Firestore schema
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.
Build the poll display page with option cards and percentage bars
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.
Implement the vote action with duplicate prevention
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.
Write the vote atomically using a Cloud Function transaction
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.
1// Cloud Function: castVote2const functions = require('firebase-functions');3const admin = require('firebase-admin');4admin.initializeApp();56exports.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);1314 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');1920 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.
Add poll creation form and expiry countdown
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.
Build the polls list page with status badges
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
1// Cloud Function: castVote — atomic vote with duplicate prevention2const functions = require('firebase-functions');3const admin = require('firebase-admin');4admin.initializeApp();56exports.castVote = functions.https.onCall(async (data, context) => {7 if (!context.auth) {8 throw new functions.https.HttpsError('unauthenticated', 'Must be logged in');9 }1011 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);1617 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 }2223 const pollData = pollDoc.data();24 if (!pollData.isActive) {25 throw new functions.https.HttpsError('failed-precondition', 'Poll has ended');26 }2728 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 }3233 const voteDoc = await tx.get(voteRef);34 if (voteDoc.exists) {35 throw new functions.https.HttpsError('already-exists', 'You have already voted');36 }3738 if (optionIndex < 0 || optionIndex >= pollData.options.length) {39 throw new functions.https.HttpsError('invalid-argument', 'Invalid option');40 }4142 const options = [...pollData.options];43 options[optionIndex] = {44 ...options[optionIndex],45 voteCount: (options[optionIndex].voteCount || 0) + 1,46 };4748 tx.update(pollRef, {49 options: options,50 totalVotes: admin.firestore.FieldValue.increment(1),51 });5253 tx.set(voteRef, {54 optionIndex: optionIndex,55 timestamp: admin.firestore.FieldValue.serverTimestamp(),56 });5758 return { success: true, message: 'Vote recorded' };59 });60});6162// Scheduled: close expired polls63exports.closeExpiredPolls = functions.pubsub64 .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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation