Voxly (also named PollFlow in README) is a functional full-stack polling platform with solid backend architecture and good feature coverage, but suffers from significant dead code bloat. The active implementation includes JWT auth with proper middleware patterns, dynamic poll creation with question/option management, a well-validated response collection flow with duplicate prevention, analytics with MongoDB aggregation pipelines, and Socket.io real-time updates with room-based broadcasting. However, the frontend is basic (no per-question mandatory/optional toggle in active code, no edit UI, basic analytics visualization), no backend is deployed (only the Netlify frontend works), and ~1,400 lines of dead code with duplicate page implementations, empty stubs, and broken imports pollute the codebase. Scores total 44/80, placing it near the cohort median of 43.
Authentication & Access Control6 / 10
Solid JWT authentication with register, login, and getMe endpoints (authController.js, 87 lines). express-validator chains on registration (auth.js routes:12-16) validate username/email/password. bcryptjs at 12 rounds (User.js:35), findByEmail static with +password select (User.js:46-48). Two middleware: protect (auth.js:4-27) for mandatory auth and optionalAuth (auth.js:29-54) that gracefully sets req.user=null. Client AuthContext (contexts/AuthContext.jsx, 46 lines) manages user state with localStorage persistence, login/logout functions, and auto-validation on mount via getMe. ProtectedRoute checks auth before rendering (router/index.jsx:15-19). Axios interceptor auto-attaches Bearer token and redirects on 401 (axiosInstance.js:5-21). Anonymous vs authenticated poll modes enforced server-side via requiresAuth field (publicController.js:36-38, 75-77). Gaps: no refresh token rotation (single 7-day JWT), tokens in localStorage (XSS-vulnerable), no email verification, no password reset, no logout endpoint on server, no CSRF protection beyond helmet defaults.
Poll Creation & Question Management5 / 15
Active CreatePoll.jsx (198 lines) supports dynamic questions with add/remove (min 1 enforced), dynamic options per question with add/remove (min 2 enforced), duration selector (1h/24h/3d/1w/30d dropdown), and public/private access toggle. Client-side validation via validatePoll() (utils/validation.js:3-12) checks title, questions have text, options count and text. Server-side createPoll (pollController.js:5-59) validates expiry date, questions array, question text, option count/text, and generates shareToken via nanoid(10). Poll update endpoint PUT /api/polls/:pollId exists with guards against editing polls with responses or published polls (pollController.js:82-140). QuestionBuilder component exists only in dead code (not used by active router). Gaps: no per-question mandatory/optional toggle in active CreatePoll (all questions effectively required), no question reordering (move up/down), no slug/permalink, no draft status, no QR code, client-side EditPoll.jsx is empty stub (0 lines), no form library (plain useState), isRequired field accepted by server but never exposed in active client form.
Response Collection Flow6 / 15
PollResponse.jsx (147 lines) fetches poll by shareToken, renders radio-button options per question, validates all questions answered before submission, generates anonymous token via crypto.randomUUID() stored in localStorage, and listens for poll_expired socket events for auto-redirect. Server-side submitResponse (publicController.js:58-144) performs defense-in-depth: validates answers is array, checks poll exists and isActive, enforces requiresAuth, prevents duplicates via checkDuplicateResponse (pollUtils.js:4-14) using userId for authenticated or IP for anonymous users, validates all required questions answered (returns missingQuestions array), verifies each questionId and selectedOptionId belongs to the poll, saves response with respondentId/ipAddress/userAgent, increments totalResponses counter, and emits socket event. Rate limited at 5/hour per IP+shareToken (rateLimiter.js:11-20). Differentiated HTTP status codes (400/401/403/404/409/410/422). Gaps: no per-question mandatory/optional client-side (all required), no completion progress bar, no countdown timer, no option clearing for optional questions, anonymous poll server enforcement but client always creates anonToken regardless.
Analytics & Feedback Dashboard5 / 15
Server analyticsController.js (125 lines) computes comprehensive analytics: responsesByDate aggregation pipeline (lines 44-52), optionCounts aggregation with $unwind (lines 54-65), completion rate via $size query (lines 67-72), per-question breakdown with totalAnswered/skipped/option counts/percentages (lines 74-95), and zero-response handling with skeleton structure (lines 10-42). Active Analytics.jsx (117 lines) renders per-question horizontal bar charts via Recharts with colored cells and tooltips, shows total votes counter, and integrates live socket updates with toast notifications. Dashboard.jsx (127 lines) shows poll grid with status badges (DRAFT/PUBLISHED/CLOSED), response/question counts, copy-link, delete, and analytics links plus empty state. PollResults.jsx (129 lines) renders public results with percentage bars, live socket updates incrementally updating vote counts, and CTA section. Poll publish endpoint blocks if zero responses (pollController.js:157-158). Gaps: no publish button in active Analytics page, no stat cards for completion rate or response velocity, no CSV/export, no individual response viewing, no trend line chart in active code (exists only in dead PollAnalytics.jsx), analytics are basic bar charts and total count only, participationByDate from server never rendered in active client.
Frontend Experience5 / 10
Custom design system with CSS utility classes (glass, voxly-glow, brand-gradient, text-headline-xl), dark/light theme toggle persisted to localStorage (Layout.jsx:6-16), responsive grid layouts, 10 routes via React Router v6 createBrowserRouter (router/index.jsx), Material Symbols icons throughout, and consistent spacing/typography tokens. All pages handle loading states and error states via react-hot-toast. Empty states present in Dashboard (empty inbox icon with CTA). Protected routes show loading indicator during auth check. Hover effects on cards and buttons, animated pulse/gradient backgrounds. Navbar with active-state styling, user greeting, and theme toggle. Landing page with multiple sections (hero, feature grids, demos, testimonials, pricing). Gaps: no form library (plain useState hooks), no skeleton loaders (only text-based "Loading..." states), no confirmation dialogs beyond window.confirm(), tokens in localStorage instead of httpOnly cookies, no inline client-side validation on login/register forms, massive dead code (~1,400 lines) shipping unused duplicate components, "Polls" and "Analytics" nav links redirect to /dashboard (router:28-29), logo URLs point to external Google-hosted images.
Backend Architecture & API Design7 / 15
Clean layered architecture: routes→controllers→models with middleware for cross-cutting concerns. express-validator used properly in routes/auth.js (lines 12-16, 22-27) and routes/polls.js (lines 12-17, 29-34) with custom handleValidationErrors middleware (validateRequest.js, 17 lines) producing consistent 422 responses. Three Mongoose models: User.js (51 lines, bcrypt pre-save 12 rounds, findByEmail static, comparePassword method), Poll.js (119 lines, embedded QuestionSchema/OptionSchema with validators, virtuals isExpired/isActive, indexes on shareToken/createdBy/expiresAt), Response.js (56 lines, AnswerSchema subdocument, compound indexes for dedup queries). Middleware: protect (JWT verify+user lookup), optionalAuth (graceful null handling), pollOwner (loads poll once, attaches to req), rate limiting (authLimiter 10/15min, respondLimiter 5/hr keyed by IP+shareToken). Security: helmet (default), CORS restricted to CLIENT_URL, morgan in dev only. Global error handler with conditional stack trace (server.js:37-44). 12 RESTful endpoints with differentiated status codes (201/200/400/401/403/404/409/410/422). Aggregation pipelines for analytics. Gaps: no service layer (controllers use Models directly), no explicit MongoDB transactions, response submit route lacks express-validator chain (manual only), JWT_SECRET has no fallback check, no pagination on getMyPolls, no graceful shutdown, no request sanitization beyond Mongoose validation.
Real-Time Updates Using WebSockets6 / 10
Socket.io server (socket/index.js, 71 lines) configured with CORS, pingTimeout/pingInterval, and three client events: join_poll_room (joins room, tracks member count, emits joined_room confirmation), leave_poll_room, and disconnect/error handlers. Auto-expire interval runs every 60s scanning for polls expired in the last minute, emitting poll_expired to rooms (lines 46-65). Three server-emitted event types: response_update with lastAnswer+totalResponses (publicController.js:129-133), poll_published with pollId (pollController.js:169), and poll_expired from interval checker. Client useSocket.js hook (83 lines) has full reconnection config (5 attempts, 1s delay), isConnected state, connectionError tracking, joinPollRoom/leavePollRoom callbacks, and typed listener helpers (onResponseUpdate/onPollPublished/onPollExpired) with cleanup. Analytics.jsx listens for response_update with toast, PollResponse.jsx listens for poll_expired for auto-redirect, PollResults.jsx computes incremental vote updates from response_update data. Gaps: two socket connections created (SocketContext via SocketProvider creates one, useSocket hook creates another via useRef - SocketContext connection is never consumed), no authentication on socket connections, no typed events, no client-side debouncing, room re-join on reconnect relies on Socket.io defaults rather than explicit handling in component remounts.
Code Quality & Project Structure4 / 10
Clean monorepo structure with client/ and server/ directories, well-organized server (routes/controllers/models/middleware/socket/utils) and client (api/components/contexts/hooks/pages/utils/router). README.md (112 lines) with setup instructions, API docs table, environment variables, project structure, and deployment guidance. .env.example files for both client and server. Consistent file naming conventions throughout. However, significant quality issues: ~1,400 lines of dead/unused code with two competing frontend architectures (App.jsx with its own BrowserRouter, duplicate pages in pages/auth/, pages/polls/, pages/dashboard/, pages/public/ — none imported by the active router/index.jsx). Seven empty stub files (EditPoll.jsx, Footer.jsx, CountdownTimer.jsx, ErrorMessage.jsx, PollCard.jsx, OptionInput.jsx, PieChartQuestion.jsx). Dual auth contexts (context/AuthContext.jsx vs contexts/AuthContext.jsx). Dead files reference non-existent import (api/axios.js). No TypeScript, no automated tests, no ESLint/Prettier configuration, no PropTypes. Console.log statements in production code (socket connections, MongoDB connection). Hardcoded external Google image URLs for logo in Layout.jsx. CSS design tokens undocumented. No graceful shutdown. Clean active code structure but the dead code situation nearly doubles the codebase with unused implementation.