Skip to main content

Polls - Developer Guide

This guide covers the technical architecture, database schema, and implementation details of the Live Polls feature in Pawtograder.

Overview

The polling system enables instructors to create quick, real-time polls during class. It uses SurveyJS for rendering poll questions and Supabase realtime for instant result updates.

Architecture

Tech Stack

LayerTechnology
FrontendNext.js 15, React 18, Chakra UI
Poll EngineSurveyJS (survey-core, survey-react-ui)
BackendSupabase (PostgreSQL + Row Level Security)
Real-TimeSupabase Realtime (broadcast channels)
ChartsRecharts
QR Codesqrcode (SVG generation)

Key Dependencies

Database Schema

Tables

live_polls

Main table storing poll definitions.
Key Fields:
  • question: Stores SurveyJS-formatted JSON containing the poll structure
  • is_live: Controls whether the poll is active (accepting responses)
  • deactivates_at: Auto-deactivation timestamp (1 hour from when poll goes live)
  • require_login: Whether anonymous responses are allowed
  • created_by: Instructor/grader who created the poll (auto-set via trigger)
While the schema default is '[]'::jsonb (empty array), the application always stores objects in SurveyJS format with an elements property, never plain arrays. All poll creation code provides explicit values, so the default is not used in practice.
Actual stored format:
Display format (UI only): When rendered in the poll interface, this is wrapped in a pages structure for SurveyJS display:

live_poll_responses

Stores individual student responses.
Key Features:
  • Allows anonymous responses (public_profile_id can be NULL when require_login is false)
  • Unique constraint ensures one response per user per poll
  • Auto-sets submitted_at timestamp via trigger

Anonymous Response Behavior

The UNIQUE constraint does not prevent duplicate anonymous responses.
The constraint UNIQUE (live_poll_id, public_profile_id) enforces one response per authenticated user, but due to PostgreSQL’s NULL handling, anonymous users (where public_profile_id is NULL) can submit unlimited responses to the same poll.

Current Behavior by User Type

User TypeDeduplicationBehavior
Authenticated (require_login = true)Enforced by UNIQUE constraintOne response per user per poll
Anonymous (require_login = false)Not enforcedUnlimited responses allowed

Recommendation

  • For public/informal polls: Accept unlimited anonymous responses as a trade-off for accessibility
  • For critical polls (grades, official votes): Use require_login = true

Database Triggers

TriggerTablePurpose
set_live_poll_created_bylive_pollsAuto-sets created_by to authenticated user on INSERT
set_poll_deactivates_atlive_pollsSets deactivates_at to 1 hour when is_live becomes true
set_response_submitted_atlive_poll_responsesAuto-sets submitted_at when is_submitted flips to true
broadcast_live_poll_changelive_pollsBroadcasts changes to realtime channels
broadcast_live_poll_response_changelive_poll_responsesBroadcasts new responses to staff channel

Auto-Deactivation

Polls automatically close after 1 hour via a scheduled function:

Row-Level Security (RLS)

Poll Policies

PolicyRoleAccess
live_polls_selectAllRead polls for classes user belongs to
live_polls_insert_staffInstructors, GradersCreate polls in their classes
live_polls_update_staffInstructors, GradersUpdate polls (cannot change created_by)
live_polls_delete_staffInstructors, GradersDelete polls in their classes

Response Policies

PolicyRoleAccess
live_poll_responses_select_staffInstructors, GradersRead all responses for their class polls
live_poll_responses_insertStudents/AnonymousInsert responses (see security function below)

Security Function for Responses

TypeScript Types

File Structure

React Hooks

useLivePolls()

Fetches all polls for a course with real-time updates.

useActiveLivePolls()

Fetches only is_live=true polls with loading state.

usePollResponseCounts(pollId, pollQuestion)

Real-time response count tracking per choice.
Implementation Details:
  • Fetches initial response counts from database
  • Subscribes to ClassRealTimeController for INSERT events
  • Directly increments counts on new responses (instant UI updates)
  • Tracks seen response IDs to avoid double-counting
  • Filters out “other:” responses

Real-Time Synchronization

Broadcast Channels

Polls use Supabase realtime broadcast for instant updates:
ChannelFormatSubscribers
Staffclass:{class_id}:staffInstructors, graders
Studentclass:{class_id}:user:{profile_id}Individual students

Real-Time Flow

  1. Instructor creates poll: INSERT broadcast to staff + all students
  2. Student submits response: INSERT broadcast to staff only
  3. Instructor closes poll: UPDATE broadcast to staff + all students
  4. Dashboard receives update: Hook updates local state, UI re-renders

Testing

E2E Tests

Poll E2E tests are located at tests/e2e/polls.test.tsx.

Test Scenarios

  • Student sees empty state with no polls
  • Real-time visibility (student sees poll go live without refresh)
  • Student can answer active polls
  • Instructor sees empty manage state
  • Visual builder updates JSON
  • Filters work (all/live/closed)
  • Response counts update in real-time
  • Poll deletion cascades to responses

Troubleshooting

Poll not appearing for students

  1. Check is_live is true
  2. Verify deactivates_at has not passed
  3. Confirm student belongs to the same class
  4. Check RLS policies are not blocking access

Real-time updates not working

  1. Verify Supabase realtime is enabled for the tables
  2. Check broadcast triggers are installed
  3. Confirm ClassRealTimeController subscription is active
  4. Check browser console for websocket errors

Response not saving

  1. Verify the poll’s require_login setting
  2. If login required, confirm student’s profile_id is correct
  3. Check for unique constraint violations (duplicate responses)
  4. Verify RLS policies allow the insert