You launch a corporate poll for 10,000 employees, and within a minute the server crashes under load. Or the results show 120% votes — someone rigged it. Sound familiar? We solve these problems every day. We develop mobile voting apps turnkey, from simple polls to complex systems with anonymity and real-time analytics. If you need a reliable solution for gathering opinions, contact us and we'll select the optimal stack. Our track record: 50+ voting projects, some serving up to 100,000 users.
A typical mobile voting form hides serious technical challenges: vote idempotency, double-tap protection, network loss handling, and result synchronization for thousands of concurrent participants. And that's just the beginning.
How to Protect Voting from Rigging?
The most critical part is ensuring each user votes only once. On the server, we use a unique constraint (poll_id, user_id) in PostgreSQL — the only reliable defense against duplicates under parallel requests. On the client, we add optimistic UI: immediately show the selection, disable retap, and queue the request on network errors with exponential backoff.
How to Implement Idempotency on the Server
To avoid duplicate votes even on network failures, use a unique database index and client-side checks. Step by step:
- Create a
votestable with unique constraint(poll_id, user_id). - On the client, block repeated taps after submission.
- On network error, save the request to a local queue and retry with exponential delay.
- On the server, handle insertion conflicts — return
409 Conflictfor duplicate votes.
This guarantees integrity without table locks.
// Android — double-tap protection viewModel.castVote(optionId) // ViewModel fun castVote(optionId: String) { if (_voteState.value is VoteState.Loading) return viewModelScope.launch { _voteState.value = VoteState.Loading _selectedOption.value = optionId repository.castVote(pollId, optionId) .onSuccess { _voteState.value = VoteState.Success } .onFailure { error -> _selectedOption.value = null _voteState.value = VoteState.Error(error) } } } Why Real-Time Is a Must, Not an Option
For live result updates without page refresh, we use Server-Sent Events (SSE) or WebSocket. SSE is preferable: unidirectional stream from server, simpler proxying and CDN, built-in reconnect. For polls with under 1,000 participants, SSE establishes connections 2x faster than WebSocket and saves up to 40% bandwidth. For enterprise polls with thousands of participants, we use WebSocket via socket.io or native URLSessionWebSocketTask on iOS / OkHttp WebSocket on Android.
| Technology | Advantages | Disadvantages |
|---|---|---|
| SSE | Simplicity, automatic reconnect, CDN compatibility | Unidirectional only, no old browser support |
| WebSocket | Bidirectional, low latency | Harder to set up, needs balancer support |
On Flutter, we connect SSE using the http package:
final stream = http.Client() .send(http.Request('GET', Uri.parse('$baseUrl/polls/$pollId/results/stream'))) .asStream() .expand((response) => response.stream .transform(const Utf8Decoder()) .transform(const LineSplitter()) .where((line) => line.startsWith('data: ')) .map((line) => PollResult.fromJson(json.decode(line.substring(6))))); Anonymity with Verification
Some scenarios require: results anonymous, but each participant is a real verified person. We implement via one-time voting tokens: on authentication, the user receives an anonymous token that the server cannot link to identity after issuance. The vote is sent with this token, not user_id. For complex cases, Zero-Knowledge Proof, but for corporate polls, one-way hashing suffices: vote_token = HMAC(user_id + poll_id, secret), where secret is known only to the server and destroyed after poll ends.
Additional verification for anonymous voting
If you need only verified users to vote anonymously, use one-time tokens. On login, the server emits a token unlinkable to the user. The vote is sent with the token, not user_id. To destroy the link after voting, use a salted hash.
Question Types and Their Implementation
| Type | Implementation Features |
|---|---|
| Single choice | Radio buttons, idempotent vote endpoint |
| Multiple choice | Checkboxes, min/max validation |
| Rating scale (NPS) | Slider or buttons 1–10, neutral state |
| Ranked choice | Drag-and-drop, ReorderableListView (Flutter) |
| Open text | TextEditingController, character limit, moderation |
| Matrix / grid | Custom component, heavy on narrow screens |
The most labor-intensive type is Ranked choice. On iOS, we use UICollectionViewDiffableDataSource with drag interaction; on Android, ItemTouchHelper.
Notifications and Poll Lifecycle
Push notifications for poll start and end via Firebase Cloud Messaging. On iOS: UNNotificationServiceExtension customizes the notification — adds results or progress bar without opening the app, as per App Store Review Guidelines section 5.1. On Android: NotificationCompat.BigPictureStyle for rich notifications with percentages.
Server response time under 10,000 concurrent votes is less than 50 ms. Throughput — 10,000 requests per second.
What's Included
- Requirements audit: question types, scale, anonymity needs
- Data schema and API design
- Mobile client development (iOS/Android/Flutter/React Native)
- Integration testing for concurrent voting
- Load testing (up to 100,000 simultaneous requests)
- App Store and Google Play publishing
- Documentation and client team training
- 30 days post-launch support
Our experience: 5+ years in mobile development, 50+ voting projects, including apps for 100,000+ users. We'll assess your project — contact us for a consultation, and we'll propose a turnkey solution.
Work Process
Audit → design → development → integration testing → load testing → publishing.
Timeline
Simple app with one question type and basic analytics: from 4 to 6 weeks. Full platform with multiple types, real-time, anonymity, and admin panel: from 3 to 4 months. Cost is individually calculated.







