Back to Projects

SwiftPolls: Real-time Audience Engagement

7 min readCase Study

SwiftPolls is a modern, full-stack web application designed to transform audience engagement through real-time interactive sessions and asynchronous surveys. Built with a robust TypeScript stack, SwiftPolls bridges the gap between live presentation tools (like Kahoot or Mentimeter) and traditional form builders (like Google Forms), offering a unified platform for creators, educators, and teams to gather instant feedback and actionable insights.

This case study explores the product design, technical architecture, and engineering solutions behind SwiftPolls, specifically focusing on how the platform handles high-concurrency real-time voting and seamless state synchronization.

1. The Problem Context

In today's hybrid work and learning environments, keeping an audience engaged is notoriously difficult. Presenters often struggle with:

  • Tool Fragmentation: Using one platform for live, interactive meeting polls and a completely different tool for post-meeting asynchronous feedback.
  • Friction in Participation: Forcing users to create accounts or download apps just to answer a single question severely drops participation rates.
  • Performance Bottlenecks: "Thundering herd" scenarios where hundreds or thousands of participants submit votes at the exact same second can easily overwhelm a traditional relational database, causing latency or crashes.

2. The SwiftPolls Solution

SwiftPolls was engineered to solve these exact pain points by offering two distinct modes of operation under one roof:

  • Live Mode: Presenters generate a unique 6-character room code. The audience joins instantly via their mobile or desktop browsers without needing an account. The presenter controls the pace, pushing questions to audience screens and revealing real-time charts as votes stream in.
  • Async Mode: Creators build a survey, set an optional expiration date, and share a public link. Respondents can answer at their own pace, and the system automatically closes the poll when the deadline hits.

Key Product Features

  • Intuitive Poll Builder: A drag-and-drop interface (powered by React Hook Form and Zod) to construct multi-question polls with custom settings (e.g., mandatory questions, live result visibility, time limits).
  • Presenter Dashboard & Controls: A dedicated interface for hosts to manage active sessions, toggle answer locks, push next questions, and end sessions.
  • Rich Analytics: Beautiful, interactive dashboards utilizing Recharts to display bar charts, pie charts, and response trends.
  • Frictionless Audience Experience: A mobile-optimized, real-time voting interface that updates instantly without page reloads.

3. Technical Architecture & Stack

SwiftPolls employs a modern, type-safe stack across both the client and the server, ensuring developer velocity and application reliability.

The Frontend (Client)

  • Framework: React 19 built with Vite for lightning-fast HMR and optimized builds.
  • Language: TypeScript.
  • State Management & Data Fetching: TanStack Query (React Query) v5 handles server state, caching, and optimistic updates.
  • Real-time: socket.io-client for persistent WebSocket connections.
  • Styling: Tailwind CSS v4 for utility-first, responsive design, combined with lucide-react for iconography.
  • Forms & Validation: react-hook-form paired with zod for strict client-side validation that mirrors backend schemas.

The Backend (Server)

  • Runtime: Node.js with Express 5.
  • Database: PostgreSQL, providing strong relational guarantees for users, polls, and response data.
  • ORM: Drizzle ORM, offering a lightweight, hyper-performant, and type-safe database layer.
  • In-Memory Store: Redis (via ioredis), utilized for caching, session management, and rate limiting.
  • Background Jobs: BullMQ, backed by Redis, to handle intensive background tasks and scheduled events.
  • Real-time Engine: Socket.IO server, deeply integrated with Express to broadcast state changes.
  • Authentication: A dual-strategy approach using JWT (Access/Refresh tokens) and Passport.js for Google OAuth 2.0.

4. Engineering Challenges & Solutions

Building a platform that must instantly reflect state across multiple clients while safely recording data at scale presented several distinct architectural challenges.

Challenge A: The "Thundering Herd" Voting Problem

In a live session, a presenter might ask a question to an audience of 1,000 people. Human behavior dictates that a vast majority of those 1,000 people will tap their screens within a tight 3-to-5-second window.

If the backend executed a standard SQL UPDATE analytics SET count = count + 1 or INSERT for every single incoming vote, the PostgreSQL database would face severe row-level lock contention and connection pool exhaustion, leading to massive latency spikes.

The Solution: Redis Buffering & BullMQ Flush Workers

Instead of writing directly to Postgres, SwiftPolls utilizes Redis as a high-throughput, in-memory buffer.

  1. Atomic Increments: When a vote hits the /api/responses endpoint, the backend uses Redis's highly optimized, atomic INCR command: redis.incr('analytics:${poll_id}:${option_id}'). This operation takes microseconds and never blocks the database.
  2. Real-time Broadcast: The backend immediately broadcasts the updated counts to the specific Socket.IO room, ensuring the presenter's screen shows the bar charts climbing instantly.
  3. The Analytics Worker: To ensure data permanence without hurting performance, SwiftPolls employs a BullMQ repeatable job named analytics-snapshot. Every 10 seconds, this worker wakes up:
    • It scans Redis for all analytics:* keys using a pipeline.
    • It retrieves the current buffered counts and atomically decrements them (using DECRBY).
    • It takes this aggregated batch of votes and performs a bulk UPSERT transaction into PostgreSQL.

Result: The PostgreSQL database only receives a handful of optimized queries every 10 seconds, regardless of whether there are 10 or 10,000 concurrent voters.

Challenge B: Synchronizing Live Session State

In live mode, the audience's screens must precisely mirror the presenter's intent. If the presenter moves to Question 2, all audience devices must instantly render Question 2. If the presenter locks voting, all audience buttons must instantly disable.

The Solution: Socket.IO Rooms and Event-Driven State

SwiftPolls leverages Socket.IO's "Rooms" feature to segment traffic.

  • When a session starts, a unique room_code is generated.
  • Audience members join, emitting a join_room event. The backend places their socket connection into that specific room.
  • The Presenter acts as the authoritative source of truth. When they click "Next Question", the client fires an API call and emits a next_question WebSocket event.
  • The backend updates the session's current_question_index in PostgreSQL (for persistence if someone reconnects) and immediately broadcasts io.to(room_code).emit("question_changed", payload).
  • The React client listens for these events via a custom SocketContext and updates local state, triggering instant UI re-renders for the audience.

Challenge C: Precision Scheduling for Async Polls

Async polls allow creators to set an expiration date (e.g., "Close this survey on Friday at 5:00 PM"). The system must accurately close the poll at that exact millisecond and optionally notify the creator, without running expensive, continuous database polling (like a cron checking the DB every minute).

The Solution: BullMQ Delayed Jobs

When a creator publishes an async poll with an expires_at timestamp, the backend calculates the exact millisecond delay: delay = expiresAt.getTime() - Date.now().

It then adds a job to the BullMQ poll-expiry queue with that specific delay. Redis holds this job efficiently in memory. When the precise moment arrives, the BullMQ worker wakes up, processes the job, sets is_active: false in the database, pushes a WebSocket event to boot any active respondents, and uses Nodemailer to send a "Poll Expired" summary email to the creator.

5. Security & Data Integrity

Handling user data and preventing abuse is critical for a polling platform.

  • Rate Limiting: To prevent malicious users from spamming votes, the express-rate-limit middleware restricts the /api/responses endpoints to 5 submissions per minute per IP address.
  • Vote Deduplication:
    • Authenticated Users: The backend enforces a unique constraint based on poll_id and user_id.
    • Anonymous Async Polls: The backend issues a unique session_token via crypto.randomUUID() when a user loads the poll, storing it in Postgres alongside the vote to prevent double voting from the same browser session. Client-side localStorage deduplication provides an immediate UX block.
  • Authentication Security: JWTs are used for stateless auth. The short-lived Access Token (15 mins) is kept in memory/local state, while the long-lived Refresh Token (7 days) is securely stored in an HttpOnly, Secure, SameSite=none cookie, drastically reducing the risk of XSS attacks stealing credentials.

6. Conclusion

SwiftPolls demonstrates how modern web technologies can be orchestrated to build a highly responsive, scalable, and engaging platform. By offloading high-frequency write operations to Redis, utilizing BullMQ for asynchronous processing, and maintaining tight client-server synchronization via Socket.IO, the architecture elegantly sidesteps common bottlenecks associated with real-time applications.

The resulting product provides presenters with a powerful, zero-latency tool to engage their audiences, wrapped in a clean, user-friendly interface that requires zero friction for participants to join.