← Pipeline

πŸ›οΈ TownHall β€” Live Anonymous Q&A & Idea Voting for Communities

Date: 2026-07-11 Idea ID: 019 Category: community, events Difficulty: ⭐⭐ (Intermediate) Estimated Build Time: 2–3 days (solo developer) Tech Stack: Next.js 15 (App Router) Β· React 19 Β· TypeScript 5.5 Β· PostgreSQL 16 (Supabase) Β· Drizzle ORM 0.38 Β· Supabase Auth Β· Supabase Realtime Β· Tailwind CSS v4 Β· shadcn/ui Β· Framer Motion Β· Stripe Β· Resend Β· Sentry Β· PostHog Β· Vercel


🎯 Elevator Pitch

TownHall is the anonymous Q&A and idea-voting board your all-hands, meetup, or conference has been missing. Create a session, share a link, and watch questions roll in β€” ranked by upvotes, filtered by relevance, and answered in real-time. No sign-up required for participants. No more awkward silences or chat-scroll fatigue.


πŸ”₯ Problem

  1. People don't ask questions in large meetings. Fear of judgment, "that's a dumb question," social hierarchy, and plain old stage fright silence the people who need answers most.
  2. Chat is chaos. In virtual meetings, questions fly into a side chat that nobody reads. In-person, nobody writes them down at all. Good questions get buried, duplicates pile up, and the organizer has no pulse on what matters most.
  3. The loudest voices win. In every all-hands, Q&A is dominated by the same 3 people. The quiet majority β€” new hires, juniors, remote workers β€” never get their questions heard.
  4. No prioritization. Even when questions are collected, there's no democratic way to surface the most important ones. Organizers guess at what matters.
  5. Enterprise tools are overkill and overpriced. Slido ($124+/mo), Mentimeter ($84+/mo), and Poll Everywhere are designed for enterprise conference organizers, not for a weekly team standup or a 50-person community call.

Current workflow for a community organizer:

  1. "Any questions?" β†’ silence β†’ awkward pause β†’ move on
  2. Type questions into Google Docs / Notion during the call
  3. Forget to follow up
  4. Repeat next week

βœ… Solution

TownHall is a dead-simple, beautiful, real-time anonymous Q&A board:

  • Create a session β€” one click, zero configuration. You get a shareable link.
  • Participants submit questions anonymously β€” no sign-up, no app install, just open the link and type.
  • Live voting β€” upvote questions you want answered. Real-time ranking via WebSocket.
  • Organizer dashboard β€” see all questions ranked by votes, mark as "answered," pin to top, or dismiss.
  • Moderation controls β€” approve/reject questions before they go public. Block keywords. Mute participants.
  • Export β€” CSV of all Q&As for meeting notes or retrospectives.
  • Replay mode β€” after the session, anyone can view the full Q&A timeline with vote history.

Key differentiator: TownHall is participant-zero-friction β€” no account, no email, no CAPTCHA. Show up, type, vote. That's it. For organizers, it's one-click setup β€” no onboarding wizard, no template selection, no configuration. The product surfaces in under 10 seconds.


πŸ‘₯ Target User

PersonaDescriptionPain LevelWillingness to Pay
Engineering ManagerRuns weekly all-hands for 20-100 person eng org. Tired of silence during Q&A.πŸ”΄ High β€” wants inclusive culture$15-29/mo
Meetup OrganizerRuns a community meetup (React, Python, etc.) with 50-300 attendees. Needs to keep Q&A organized.πŸ”΄ High β€” chaotic in-person Q&A$19-39/mo
Conference Speaker/Workshop LeadSpeaks at conferences, runs workshops. Wants to collect questions without being overwhelmed.🟑 Medium β€” nice-to-have during talksPay-per-event ($5/event)
Remote Team LeadManages a distributed team. Struggles with async Q&A across time zones.🟑 Medium β€” wants async input$15-29/mo
Product ManagerRuns customer feedback sessions, user research groups. Needs structured Q&A.🟑 Medium β€” needs exportable results$29-49/mo
Community Manager (Discord/Slack)Manages 500+ person community. Wants structured feedback without channel spam.🟑 Medium β€” needs weekly pulse$19-39/mo

πŸ› οΈ Full Tech Stack

Frontend (Main App)

LayerTechnology
FrameworkNext.js 15 (App Router) + React 19 + TypeScript 5.5
StylingTailwind CSS v4 (utility-first, @apply sparingly)
Component Libraryshadcn/ui (Radix Primitives β€” Button, Card, Dialog, Sheet, Input, Textarea, Badge, Separator, Tabs, Tooltip)
AnimationFramer Motion 11.x (question card transitions, vote animations, real-time updates)
IconsLucide React (Tree-shakeable, MIT license)
State ManagementReact Server Components (default) + TanStack React Query 5.x for client mutations + real-time subscriptions
Form HandlingReact 19 useActionState (server actions for question submission, minimal client JS)
Real-time ClientSupabase Realtime JS client (WebSocket subscriptions for live vote counts + new questions)

Backend & API

LayerTechnology
API LayerNext.js Route Handlers (RESTful JSON) + React Server Actions for form submissions
DatabasePostgreSQL 16 via Supabase (managed, includes auth + realtime + storage)
ORMDrizzle ORM 0.38.x (type-safe, drizzle-kit for migrations)
AuthSupabase Auth (email magic link + Google OAuth + GitHub OAuth)
RealtimeSupabase Realtime (WebSocket subscriptions on questions and votes tables)
Rate LimitingUpstash Redis (serverless, limits: 5 questions/min per session, 1 vote/2s per IP)
Background JobsInngest (serverless queue: session-end emails, weekly reports, stale session cleanup)

Storage & CDN

LayerTechnology
File StorageSupabase Storage (session cover images, optional, < 2MB)
CDNVercel Edge Network (static assets, API response caching at edge)
Public AssetsVercel Asset Optimization (WebP conversion for uploads)

Payments & Email

LayerTechnology
PaymentsStripe (Checkout Sessions + Webhook β†’ Inngest for plan lifecycle)
EmailResend (transactional: session shared, question answered, weekly digest)
Session ManagementServer-side sessions (anonymous participants get a localStorage UUID) + Supabase Auth for organizers

Monitoring & Analytics

LayerTechnology
Error TrackingSentry (frontend + API route errors, performance traces)
Product AnalyticsPostHog (session creation funnels, engagement, vote actions)
PerformanceVercel Analytics (Web Vitals for session pages)
UptimeBetter Stack (HTTP monitoring on session public routes)

Testing & CI/CD

LayerTechnology
Unit TestingVitest + React Testing Library
E2E TestingPlaywright (session creation, anonymous Q&A, voting flow)
CI/CDGitHub Actions (lint β†’ typecheck β†’ test β†’ build on push & PR)
DeploymentVercel (automatic preview deploys on PR, production on main push)

🎨 UI Design Language

Foundation

ElementChoice
Base FrameworkTailwind CSS v4 (utility-first, minimal custom CSS)
Component Libraryshadcn/ui (Radix primitives, Tailwind-styled)
Design StyleWarm minimal with soft edges β€” clean white/purple surfaces, generous whitespace, pill-shaped badges, subtle gradients on interactive elements
Color Paletteβ€”

Color System

Primary:    #7C3AED (Violet-600)   β€” buttons, active session badge, upvote icon
Secondary:  #8B5CF6 (Violet-500)   β€” hover states, secondary CTAs
Accent:     #06B6D4 (Cyan-500)     β€” "Live" indicator, answered badge, progress pulse
Background: #FAFAFA (Neutral-50)   β€” app background
Surface:    #FFFFFF                β€” cards, modals, question bubbles
Border:     #E5E7EB (Gray-200)     β€” dividers, card borders
Text:       #111827 (Gray-900)     β€” headings, question text
Text Body:  #374151 (Gray-700)     β€” body copy, descriptions
Muted:      #9CA3AF (Gray-400)     β€” timestamps, vote counts, labels
Success:    #10B981 (Emerald-500)  β€” "answered" badge, confirmed actions
Warning:    #F59E0B (Amber-500)    β€” pending moderation, session nearing end
Error:      #EF4444 (Red-500)      β€” rejected questions, blocking errors

Typography

Headings:  Plus Jakarta Sans (sans-serif) β€” weights 600, 700, 800
Body:      Inter (sans-serif) β€” weights 400, 500
Code:      JetBrains Mono β€” export snippets, embedded links
Scale:     12 / 14 / 16 / 18 / 20 / 24 / 30 / 36 / 48 px
Line-height: 1.5 body / 1.2 headings
Letter-spacing: -0.02em for H1-H2

Spacing & Radius

  • Border radius: rounded-xl (12px) for cards and question bubbles, rounded-lg (8px) for inputs, rounded-full for badges, avatars, and the upvote button
  • Spacing: 4px grid basis β€” p-6 for cards, p-4 for question items, gap-4 for list spacing, gap-8 for section spacing
  • Shadows: shadow-sm for card elevation, shadow-lg for modals, shadow-md for the floating "New Question" fab
  • Max width: Session page capped at max-w-3xl for readability

Interaction Design

  • Upvote button: Pill button with micro-bounce animation on click (Framer Motion scale: [1, 1.3, 1] within 200ms). Vote count animates (number rolls up) with color shift from muted β†’ primary on active.
  • Question cards: Slide in from bottom on new questions (Framer Motion AnimatePresence with initial={{ opacity: 0, y: 20 }}). Answered questions fade to 80% opacity with a green checkmark badge.
  • Live indicator: Pulsing cyan dot in session header (CSS @keyframes pulse) β€” visible while session is active.
  • Participant view: Full-screen minimal β€” just a question input at the top and a scrollable feed of questions below. No nav, no settings, no distractions.
  • Organizer view: Toggle between "Live Feed" (ordered by votes, newest) and "Moderation Queue" (pending questions) via tabs. Floating action buttons to mark answered, pin, dismiss.
  • Empty state: Illustrated town hall podium with microphone graphic (CSS/SVG illustration) β€” "No questions yet. Be the first to ask!"

Dark Mode

  • Strategy: Class-based toggle (dark class on <html>). Organizer preference persisted in Supabase user metadata. Respects prefers-color-scheme on first visit.
  • Dark palette: Background #0A0A0A, Surface #18181B (Zinc-900), Text #F5F5F5, Border #27272A (Zinc-800). Primary stays Violet but at 90% lightness (#A78BFA). Cards get a subtle border-white/[0.05] ring instead of shadow.

Responsive

  • Participant page: Mobile-first β€” single column, full-width input, stacked question cards. On desktop, max-w-3xl centered layout.
  • Organizer dashboard: Desktop-first sidebar layout on tablet+. On mobile, bottom-tab navigation (Live / Moderation / Settings).
  • Question input: Sticky at top on mobile (scrolls with page), fixed position on desktop with sticky top-4.
  • Embedded session: Responsive embed widget (for embedding a session on a conference page or Notion doc) β€” scales to container width, max 480px height.

Iconography

  • Library: Lucide React v0.450+
  • Style: Outlined, 1.5px stroke, 20x20 default on question cards, 24x24 on CTAs
  • Key icons: MessageCircleQuestion (ask question), ThumbsUp / ArrowBigUp (upvote), CheckCheck (answered), Eye (viewed), Shield (moderation), Users (participants), Timer (session timer), Download (export), Link (share), Crown (organizer), SendHorizonal (submit)

πŸ—οΈ Full System Architecture

Repository Structure

townhall/
β”œβ”€β”€ app/                          # Next.js 15 App Router
β”‚   β”œβ”€β”€ (marketing)/              # Landing page, pricing, docs
β”‚   β”‚   β”œβ”€β”€ page.tsx              # Landing page
β”‚   β”‚   β”œβ”€β”€ pricing/              # Pricing page
β”‚   β”‚   └── docs/                 # How-to guides
β”‚   β”œβ”€β”€ (dashboard)/              # Authenticated organizer routes
β”‚   β”‚   β”œβ”€β”€ layout.tsx            # Sidebar layout + Supabase listener
β”‚   β”‚   β”œβ”€β”€ page.tsx              # Dashboard home (active/recent sessions)
β”‚   β”‚   β”œβ”€β”€ sessions/             # Session CRUD
β”‚   β”‚   β”‚   β”œβ”€β”€ new/              # New session form
β”‚   β”‚   β”‚   └── [id]/             # Session organizer view
β”‚   β”‚   β”‚       β”œβ”€β”€ page.tsx      # Live Q&A management board
β”‚   β”‚   β”‚       β”œβ”€β”€ settings/     # Session settings
β”‚   β”‚   β”‚       β”œβ”€β”€ analytics/    # Question stats, vote distribution
β”‚   β”‚   β”‚       └── export/       # CSV export
β”‚   β”‚   β”œβ”€β”€ settings/             # Account, billing, preferences
β”‚   β”‚   └── sessions/             # Session list
β”‚   β”œβ”€β”€ s/                        # Public session routes (participant view)
β”‚   β”‚   └── [slug]/               # Single session (no auth required)
β”‚   β”‚       β”œβ”€β”€ page.tsx          # Participant Q&A view
β”‚   β”‚       └── replay/           # Archived session replay
β”‚   β”œβ”€β”€ api/                      # Route Handlers
β”‚   β”‚   β”œβ”€β”€ webhooks/             # Supabase Auth, Stripe, Inngest
β”‚   β”‚   β”œβ”€β”€ sessions/             # CRUD (organizer only)
β”‚   β”‚   β”œβ”€β”€ questions/            # CRUD + voting
β”‚   β”‚   β”œβ”€β”€ votes/                # Vote actions
β”‚   β”‚   └── export/               # CSV generation
β”‚   β”œβ”€β”€ auth/                     # Supabase Auth pages (login, callback)
β”‚   β”œβ”€β”€ layout.tsx                # Root layout
β”‚   └── page.tsx                  # Redirect to dashboard or landing
β”œβ”€β”€ components/
β”‚   β”œβ”€β”€ session/                  # Session-specific components
β”‚   β”‚   β”œβ”€β”€ QuestionCard.tsx      # Single question with vote button
β”‚   β”‚   β”œβ”€β”€ QuestionList.tsx      # Live-updating question feed
β”‚   β”‚   β”œβ”€β”€ QuestionInput.tsx     # Anonymous question form
β”‚   β”‚   β”œβ”€β”€ VoteButton.tsx        # Animated upvote button
β”‚   β”‚   β”œβ”€β”€ LiveIndicator.tsx     # Pulsing "Live" badge
β”‚   β”‚   β”œβ”€β”€ ModerationQueue.tsx   # Pending questions moderation
β”‚   β”‚   └── SessionHeader.tsx     # Session title, participant count, timer
β”‚   β”œβ”€β”€ dashboard/                # Dashboard components
β”‚   β”‚   β”œβ”€β”€ SessionCard.tsx       # Session preview card
β”‚   β”‚   β”œβ”€β”€ StatsGrid.tsx         # Question count, vote count, answered %
β”‚   β”‚   β”œβ”€β”€ EmptyState.tsx        # First-session onboarding
β”‚   β”‚   └── RecentSessions.tsx    # Recently active sessions
β”‚   └── ui/                       # shadcn/ui primitives
β”œβ”€β”€ db/                           # Drizzle schema + migrations
β”‚   β”œβ”€β”€ schema.ts                 # All table definitions
β”‚   └── migrations/               # Generated SQL migrations
β”œβ”€β”€ lib/
β”‚   β”œβ”€β”€ db.ts                     # Drizzle client (Supabase)
β”‚   β”œβ”€β”€ auth.ts                   # Supabase server-side helpers
β”‚   β”œβ”€β”€ realtime.ts               # Supabase Realtime channel setup
β”‚   β”œβ”€β”€ rate-limit.ts             # Upstash rate limiter helper
β”‚   └── stripe.ts                 # Stripe client helpers
β”œβ”€β”€ inngest/                      # Background job functions
β”‚   β”œβ”€β”€ session-ended.ts          # Email recap to organizer
β”‚   β”œβ”€β”€ weekly-digest.ts          # Weekly activity summary
β”‚   └── cleanup-sessions.ts       # Archive stale sessions
└── public/                       # Static assets (favicon, OG images)

Architecture Pattern

  • Pattern: Single Next.js 15 repo (no turborepo β€” overkill for this scope)
  • Route groups: (marketing) for public pages, (dashboard) for authenticated routes
  • Public session routes: /s/[slug] β€” no auth, renders with React Server Components, real-time via Supabase Realtime client-side subscription
  • Bundle splitting: App Router auto-splits. Public session page loads minimal JS (~40KB gzipped) β€” no dashboard code on participant pages.

API Design

  • Style: RESTful JSON via Next.js Route Handlers + React Server Actions for form submissions (question creation, voting)
  • Endpoints:
EndpointMethodPurpose
/api/sessionsGETList organizer's sessions (paginated)
/api/sessionsPOSTCreate new session
/api/sessions/[id]GETFetch session details (organizer)
/api/sessions/[id]PATCHUpdate session settings
/api/sessions/[id]DELETEArchive/delete session
/api/sessions/[id]/questionsGETList questions with votes (public + organizer)
/api/sessions/[id]/exportGETDownload CSV export
/api/questionsPOSTSubmit question (anonymous, requires session slug)
/api/questions/[id]PATCHUpdate question (moderation: approve/reject, mark answered)
/api/questions/[id]DELETERemove question (organizer only)
/api/votesPOSTToggle upvote on question
/api/webhooks/supabasePOSTHandle Auth user lifecycle
/api/webhooks/stripePOSTHandle subscription changes

Server Actions (React 19)

ActionInputPurpose
submitQuestion(formData)sessionSlug, textCreates question (anonymous user)
toggleVote(formData)questionId, sessionSlugToggle upvote (anonymous user)
createSession(formData)title, settingsCreates session (organizer)
markAnswered(formData)questionIdMarks question as answered (organizer)

Using Server Actions for participant-facing operations (question submit, vote) minimizes client JS and enables progressive enhancement. API routes handle organizer CRUD operations and webhooks.

Database Schema

-- Core tables

sessions
  id: uuid PK (default gen_random_uuid())
  organizer_id: uuid FK -> auth.users (Supabase Auth)
  title: text (required, max 200 chars)
  slug: text (unique, URL-safe, auto-generated from title)
  description: text (optional, max 500 chars)
  status: enum: active, paused, ended (default: active)
  moderation_enabled: boolean (default: false)
  anonymous_allowed: boolean (default: true)
  max_questions_per_participant: int (default: 10, 0 = unlimited)
  participant_count: int (approx counter, real-time)
  question_count: int (denormalized counter)
  is_public: boolean (default: true)
  cover_emoji: text (optional, e.g., "πŸ›οΈ")
  scheduled_start: timestamptz (nullable)
  ended_at: timestamptz (nullable)
  created_at: timestamptz (default now())
  updated_at: timestamptz (default now())

questions
  id: uuid PK (default gen_random_uuid())
  session_id: uuid FK -> sessions (cascade delete)
  participant_id: text (anonymous UUID stored in localStorage, nullable)
  text: text (required, max 500 chars)
  votes_count: int (default 0, denormalized counter)
  status: enum: pending, approved, answered, dismissed (default: approved)
  is_pinned: boolean (default: false)
  is_anonymous: boolean (default: true)
  answered_at: timestamptz (nullable)
  created_at: timestamptz (default now())

votes
  id: uuid PK (default gen_random_uuid())
  question_id: uuid FK -> questions (cascade delete)
  participant_id: text (anonymous UUID from localStorage)
  created_at: timestamptz (default now())
  UNIQUE(question_id, participant_id) -- one vote per person per question

organizer_profiles
  id: uuid PK (FK -> auth.users)
  display_name: text
  plan: enum: free, pro, business (default: free)
  stripe_customer_id: text (nullable)
  stripe_subscription_id: text (nullable)
  created_at: timestamptz (default now())

-- Indexes
CREATE INDEX idx_questions_session_status ON questions(session_id, status, votes_count DESC);
CREATE INDEX idx_questions_created ON questions(session_id, created_at DESC);
CREATE INDEX idx_votes_question ON votes(question_id);
CREATE INDEX idx_sessions_organizer ON sessions(organizer_id, created_at DESC);
CREATE INDEX idx_sessions_slug ON sessions(slug);

Auth Flow

  • Organizer auth: Supabase Auth (email magic link + Google OAuth + GitHub OAuth)
  • Session type: JWT (Supabase auto-managed, short-lived with refresh)
  • Middleware: Next.js middleware checks Supabase session β€” redirects unauthenticated users to /auth/login for dashboard routes
  • Participant auth: None. Anonymous participants get a UUID stored in localStorage (persists across browser sessions). This UUID is used as participant_id in votes and questions β€” no account, no email, no friction.
  • Rate limiting: Upstash Redis β€” 5 questions/minute per participant (by IP), 1 vote per 2 seconds per participant (by IP + participant_id). Prevents spam without requiring auth.

File Storage Strategy

  • Provider: Supabase Storage (S3-compatible, managed bucket)
  • Usage: Optional session cover images / event branding for Pro tier
  • Bucket structure: townhall/sessions/{session_id}/cover.{ext}
  • Constraints: Max 2MB, JPEG/PNG/WebP only, auto-resize via Supabase Image Transformations

Real-time Architecture

TownHall's real-time is its killer feature. Here's how it works:

1. Participant opens /s/[slug] β†’ page loads as React Server Component (initial SSR)
2. Client hydrates β†’ creates Supabase Realtime channel:
   channel = supabase.channel(`session:${slug}`)
   channel.on('postgres_changes',
     { event: '*', schema: 'public', table: 'questions',
       filter: `session_id=eq.${sessionId}` },
     (payload) => { /* update question list */ }
   )
3. Vote mutations use Server Actions:
   - Client calls toggleVote(questionId) β†’ Server Action
   - Server Action validates (rate limit, session active, question exists)
   - Inserts/deletes vote row, updates denormalized votes_count
   - Supabase Realtime broadcasts change β†’ all connected clients update
4. Organizer sees questions appearing in real-time via same channel
5. When organizer marks "answered" or "dismissed" β†’ Realtime broadcast updates all participants

This means:

  • No polling. No manual refresh. Questions and votes update in < 200ms across all clients.
  • Supabase Realtime handles WebSocket management (connection pooling, reconnection, presence).
  • Zero additional infrastructure for real-time.

Background Jobs (Inngest)

  1. session-ended β€” When organizer ends a session, sends a recap email via Resend with: total questions, top-voted question, answer rate, participant count, CSV download link.
  2. weekly-digest β€” Every Monday 9:00 UTC, sends organizers a summary of their week: sessions created, questions received, votes cast, top unanswered questions.
  3. cleanup-stale-sessions β€” Daily, auto-archives sessions that have been ended for 30+ days (moves to read-only replay mode, removes from active dashboard).
  4. auto-end-session β€” If a scheduled session's scheduled_end time passes, auto-ends the session and triggers session-ended.

Caching Strategy

LayerStrategy
Public session pageReact Server Component (fresh on every SSR β€” questions update in real-time client-side anyway, no ISR needed)
DashboardServer Components + React cache() β€” fresh on navigation. Client-side TanStack Query with staleTime: 30s for session list
Realtime updatesSupabase Realtime WebSocket β€” no manual cache invalidation needed. The database is the source of truth
Vote countsDenormalized questions.votes_count column (atomic UPDATE questions SET votes_count = votes_count + 1). No separate counter table
CDNVercel Edge Network for static assets. Supabase CDN for uploaded images
Rate limit dataUpstash Redis (serverless, ephemeral β€” TTL-based, no persistence needed)

Environment Structure

# .env.local (development) / .env.production (Vercel)
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=
DATABASE_URL=                    # Supabase connection string (pooled)
DIRECT_URL=                      # Supabase direct connection (migrations)

SUPABASE_AUTH_WEBHOOK_SECRET=

UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=

STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
NEXT_PUBLIC_STRIPE_PRICE_IDS=   # JSON: {pro: "price_xxx", business: "price_yyy"}

RESEND_API_KEY=
RESEND_AUDIENCE_ID=              # For newsletter/onboarding

INNGEST_EVENT_KEY=
INNGEST_SIGNING_KEY=

NEXT_PUBLIC_APP_URL=            # https://townhall.app

SENTRY_DSN=
NEXT_PUBLIC_POSTHOG_KEY=
NEXT_PUBLIC_POSTHOG_HOST=

Deployment Topology

[Participant Browser]
     β”‚
     β”œβ”€β”€ https://townhall.app ──────────── Vercel Edge Network
     β”‚    β”œβ”€β”€ / (Landing page)           ── SSG, revalidate 300s
     β”‚    β”œβ”€β”€ /s/[slug] (Session)        ── SSR + client Realtime subscription
     β”‚    β”œβ”€β”€ /dashboard/*               ── SSR + Server Actions
     β”‚    └── /pricing                   ── SSG, revalidate 3600s
     β”‚
     β”œβ”€β”€ Supabase Realtime ────────────── WebSocket (long-lived persistent connection)
     β”‚
     └── Supabase Auth ────────────────── OAuth / Magic link flows
                    β”‚
[Vercel Serverless Functions]
     β”‚
     β”œβ”€β”€ Route Handlers ───────────────── Supabase PostgreSQL (via Drizzle)
     β”œβ”€β”€ Server Actions ───────────────── Supabase PostgreSQL (via Drizzle)
     β”œβ”€β”€ Inngest Functions ────────────── Supabase PostgreSQL (via Drizzle)
     β”œβ”€β”€ Webhooks (Stripe, Supabase) ──── Resend (email), Sentry (errors)
     β”‚
     └── Upstash Redis ───────────────── Rate limiting (ephemeral)

[Supabase]
     β”œβ”€β”€ PostgreSQL 16 ───────────────── Main data (sessions, questions, votes)
     β”œβ”€β”€ Auth ────────────────────────── User management, JWT sessions
     β”œβ”€β”€ Realtime ────────────────────── WebSocket broadcast on table changes
     └── Storage ─────────────────────── Session cover images (Pro tier)

Monitoring

ToolWhat It Monitors
SentryJavaScript errors (frontend + API routes), Realtime connection failures
PostHogSession creation funnel, question submission rate, vote conversion, retention
Vercel AnalyticsWeb Vitals (LCP, CLS, INP), public session page load times
Better StackHTTP uptime for public session routes and API
Supabase DashboardDB query performance, Realtime active connections, auth rate limits

Data Flow Diagram (Description)

Submitting a question (anonymous participant):

  1. Visitor opens /s/my-allhands β†’ React Server Component renders initial SSR with existing questions (sorted by votes).
  2. Client hydrates β†’ Supabase Realtime channel subscribes to questions for this session.
  3. Visitor types question β†’ clicks "Submit" β†’ React 19 Server Action fires.
  4. Server Action: validates question length (max 500 chars), checks rate limit via Upstash Redis (5/min per IP), checks session is active, inserts question row into Postgres via Drizzle.
  5. Supabase Realtime detects the INSERT on questions table β†’ broadcasts to all subscribed clients (including the submitter and all other participants).
  6. All connected clients update their question list in real-time β€” new question card slides in at the bottom (if no votes) or ranks by votes immediately.
  7. Organizer's dashboard updates simultaneously β€” question appears in their moderation queue (if moderation enabled) or directly in the live feed.

Voting on a question:

  1. Visitor clicks upvote button β†’ Server Action fires with questionId and participantId.
  2. Server Action: checks rate limit (1 vote/2s per IP + participant), checks question exists in active session, upserts row in votes table, atomically increments questions.votes_count.
  3. Supabase Realtime broadcasts the update β†’ all clients update the vote count with micro-animation (number rolls up).
  4. If the vote pushes this question to #1, it animates to the top of the list (Framer Motion layout animation layoutId).

Organizer answering a question:

  1. Organizer sees question in dashboard β†’ clicks "Mark Answered" β†’ PATCH to /api/questions/[id] with status: answered.
  2. Server validates ownership (question belongs to organizer's session) β†’ updates row.
  3. Realtime broadcast β†’ participant client shows green checkmark + "Answered by organizer" badge on that question card.
  4. Question drops to 80% opacity, sorted below unanswered questions (answered questions move down in priority).

πŸ“‹ PRD β€” Functional Requirements

FR-01: Session Creation & Management

IDRequirementPriorityNotes
FR-01.1User can sign up with email magic link, Google, or GitHub (Supabase Auth)P0
FR-01.2User can create a new session with title, optional description, and optional emojiP0Slug auto-generated from title
FR-01.3User can set session to active, paused, or endedP0Toggle in header
FR-01.4User can enable/disable moderation (questions require approval before appearing)P0Toggle in session settings
FR-01.5User can enable/disable anonymous submissionsP1Default: enabled
FR-01.6User can set a max questions-per-participant limitP1Default: 10, 0 = unlimited
FR-01.7User can delete/archive a sessionP0Archive preserves data, delete removes everything
FR-01.8User can duplicate an existing session (reuse settings for recurring events)P1
FR-01.9User can schedule a session for a future date/timeP2

FR-02: Anonymous Q&A (Participant View)

IDRequirementPriorityNotes
FR-02.1Participant can open a session link with no sign-up requiredP0Just the URL, nothing else
FR-02.2Participant can submit a question (text, max 500 chars)P0Server Action, rate-limited
FR-02.3Participant's question appears in the feed immediately (or goes to moderation queue if enabled)P0Realtime update
FR-02.4Participant can upvote any question (one vote per question per participant)P0Toggle on/off
FR-02.5Participant can see question list sorted by votes (highest first) by defaultP0Server-sorted, real-time updates
FR-02.6Participant can toggle between "Top Voted" and "Newest" sort ordersP1Client-side sort
FR-02.7Participant sees "Answered" badge on questions the organizer has addressedP0
FR-02.8Participant sees live participant count in session headerP1Supabase Realtime presence
FR-02.9Participant can report a question as inappropriateP2Flags for organizer review

FR-03: Organizer Dashboard (Live Moderation)

IDRequirementPriorityNotes
FR-03.1Organizer sees a live feed of all questions sorted by votesP0Real-time updates
FR-03.2Organizer can approve or reject pending questions (moderation queue tab)P0
FR-03.3Organizer can mark a question as "Answered"P0Shows visual badge to participants
FR-03.4Organizer can pin a question to the top of the feedP1Stays at top regardless of votes
FR-03.5Organizer can dismiss/hide a question without answeringP1
FR-03.6Organizer can delete a question (removes from feed entirely)P0Cascade deletes votes
FR-03.7Organizer sees session stats header: questions count, votes count, answered %, participantsP1Real-time counters
FR-03.8Organizer can share the session link via copy button or QR codeP0

FR-04: Session Analytics & Export

IDRequirementPriorityNotes
FR-04.1Organizer can view analytics per session: total questions, total votes, answered %, peak question rateP1
FR-04.2Organizer can export session Q&A as CSV (question text, votes count, status, timestamp)P1
FR-04.3Organizer can view a timeline of when questions were submitted (activity graph)P2Recharts bar chart
FR-04.4Organizer can see which questions were most divisive (high votes but left unanswered)P2
FR-04.5Organizer receives a session recap email when they end a sessionP1Inngest + Resend

FR-05: Account & Billing

IDRequirementPriorityNotes
FR-05.1User can manage their account (name, avatar, email preferences)P0Supabase Auth
FR-05.2User can view current plan, upgrade, or cancel via Stripe Customer PortalP0
FR-05.3Free tier allows 3 active sessions, unlimited questions per session, basic analyticsP0
FR-05.4Pro tier ($19/mo) allows 15 active sessions, custom branding, moderation, CSV exportP0
FR-05.5Business tier ($39/mo) allows unlimited sessions, priority support, team accounts, white-labelP1
FR-05.6User can cancel anytime β€” sessions remain accessible but read-only after plan downgradeP0Stripe webhook
FR-05.7Trial: 14 days full-featured Pro (no credit card required)P0

FR-06: Moderation & Safety

IDRequirementPriorityNotes
FR-06.1Organizer can block specific keywords (questions containing them auto-rejected)P1Configurable list in session settings
FR-06.2Organizer can ban a participant by their anonymous participant IDP1
FR-06.3System rate-limits question submissions (5/min per IP)P0Upstash Redis
FR-06.4System rate-limits voting (1 vote per 2s per participant)P0Prevents vote manipulation
FR-06.5AI moderation (optional, Pro tier): GPT-4o-mini flags toxic/spam questionsP2Inngest step function, runs async

Non-Functional Requirements

IDRequirementTargetNotes
NFR-01Public session page loads < 1.5s on 3G (FCP)≀ 1.5sRSC + minimal client JS (~40KB gzipped)
NFR-02Question submission < 500ms (p95)< 500msServer Action + direct DB insert
NFR-03Vote propagation to all clients < 200ms (p95)< 200msSupabase Realtime (WebSocket)
NFR-0499.9% uptime for public session routes99.9%Vercel Edge + Supabase managed
NFR-05Support 500+ concurrent participants per session500+Supabase Realtime connection limit: 500 per project (scalable)
NFR-06No PII stored for anonymous participantsβ€”participant_id is random UUID, not email/name/IP
NFR-07All public endpoints rate-limitedβ€”Upstash Redis, configurable thresholds
NFR-08CSP headers on all routes (XSS protection)β€”Content-Security-Policy via Next.js middleware
NFR-09Dashboard API responses < 200ms p95< 200msIndexed queries + Drizzle
NFR-10Auth session check < 100ms< 100msSupabase Auth middleware at edge

πŸ“‚ Epics & User Stories

Epic 1: Session Foundation & Auth (Scaffolding)

Estimated: Day 1 morning

StoryDescriptionAcceptance Criteria
US-1.1As a new user, I can sign up with my Google/GitHub account and land on an empty dashboardSupabase Auth flow works; dashboard shows "Create your first session" empty state
US-1.2As a user, I can create a session by giving it a title and get a shareable linkClick "New Session" β†’ enter title β†’ session created with unique slug β†’ copy link button appears
US-1.3As a user, I can see my created sessions listed on the dashboardDashboard shows session cards: title, status, question count, date created
US-1.4As a user, I can end a session or delete it from the dashboardEnd session button β†’ status changes to "ended"; Delete β†’ confirmation β†’ session removed
US-1.5As a user, I can change session settings (title, description, moderation toggle)Settings page with form fields β†’ save updates session metadata

Epic 2: Anonymous Q&A & Real-time Voting (Core Experience)

Estimated: Day 1 afternoon – Day 2 morning

StoryDescriptionAcceptance Criteria
US-2.1As a participant, I can open a session link without any sign-up and see the Q&A boardURL loads β†’ question input + question feed visible. No auth prompt. No loading spinner.
US-2.2As a participant, I can submit a question anonymously and see it appear in the feed instantlyType question β†’ click Submit β†’ card appears in feed with Realtime animation < 500ms
US-2.3As a participant, I can upvote any question and see the count update in real-timeClick upvote β†’ count increments +1 with micro-animation β†’ click again to toggle off
US-2.4As a participant, I can see questions sorted by most votes at the topFeed sorted by votes_count DESC. Top question has #1 badge.
US-2.5As a participant, I can see which questions have been answered (green checkmark badge)Answered questions show "βœ“ Answered" badge, fade to 80% opacity

Epic 3: Organizer Live Moderation Board (Power User)

Estimated: Day 2

StoryDescriptionAcceptance Criteria
US-3.1As an organizer, I can see a live-updating board of all questions with vote countsDashboard opens β†’ real-time question list sorted by votes, with vote count badges
US-3.2As an organizer, I can mark a question as "Answered" with one clickClick checkmark icon β†’ question gets "Answered" badge β†’ participants see it in real-time
US-3.3As an organizer, I can pin an important question to the top of the feedPin icon β†’ question sticks at top with "πŸ“Œ Pinned" badge, independent of vote count
US-3.4As an organizer, I can approve or reject questions from a moderation queueModeration tab β†’ pending questions β†’ Approve (moves to live) or Reject (deletes with notification)
US-3.5As an organizer, I can share the session link or a QR code from the dashboardShare button β†’ copy link + display QR code (via qrcode.react)

Epic 4: Analytics, Export & Replay (Insights)

Estimated: Day 2 afternoon – Day 3 morning

StoryDescriptionAcceptance Criteria
US-4.1As an organizer, I can see per-session stats: total questions, votes, answered %Analytics tab shows stat cards with real-time counters
US-4.2As an organizer, I can export session Q&A as CSVExport button β†’ downloads CSV with: question text, votes, status, timestamp, answered_at
US-4.3As an organizer, I can view a question submission timeline (activity graph)Bar chart showing questions per minute/hour across session duration (Recharts)
US-4.4As a participant, I can view an ended session's replay (read-only Q&A log)Ended session link shows full question list sorted by final vote count, no new submissions allowed
US-4.5As an organizer, I receive a recap email when I end a sessionEmail sent via Resend with: total questions, top-3 voted, answer rate, participant count

Epic 5: Payments & Plans (Monetization)

Estimated: Day 3 morning

StoryDescriptionAcceptance Criteria
US-5.1As a user, I can see my current plan and upgrade on the billing pageBilling page shows plan card with features, "Upgrade" button β†’ Stripe Checkout
US-5.2As a user, I can manage my subscription via Stripe Customer Portal"Manage Subscription" β†’ Stripe-hosted portal β†’ change/cancel plan
US-5.3As a Pro user, I have access to custom branding (custom logo on session page)Session settings β†’ upload logo β†’ session page shows logo instead of default
US-5.4As a Free user, I'm limited to 3 active sessions but can upgrade anytimeAttempt to create 4th session β†’ upgrade prompt modal with feature comparison
US-5.5As a Business user, I can add team members to my accountTeam management page β†’ invite by email β†’ team members share sessions

Epic 6: Polish, Safety & Edge Cases (Launch Ready)

Estimated: Day 3 afternoon

StoryDescriptionAcceptance Criteria
US-6.1As an organizer, I can set blocked keywords that auto-reject questionsSettings β†’ "Blocked Words" textarea β†’ questions containing those words go directly to rejected
US-6.2As an organizer, I can mute a disruptive participantBan by participant_id β†’ their future submissions are silently rejected
US-6.3As a participant, I'm rate-limited if I try to spam questions5 questions in rapid succession β†’ 6th shows "Too many questions. Please wait." toast
US-6.4As a participant, duplicate questions are prevented (same text within 30s)Submit exact same question twice β†’ "This question was already asked" with link to existing
US-6.5As a user, the UI gracefully handles network disconnects (offline toast)Realtime connection drops β†’ "Reconnecting..." indicator β†’ auto-reconnects via Supabase SDK

πŸ’° Pricing Model

FeatureπŸ†“ FreeπŸš€ Pro ($19/mo)🏒 Business ($39/mo)
Active sessions315Unlimited
Questions per sessionUnlimitedUnlimitedUnlimited
Participants per session50UnlimitedUnlimited
Moderation queueβŒβœ… Accept/Reject/Blockβœ… + Auto-keyword blocking
Custom branding❌ (TownHall logo)βœ… (Upload your logo)βœ… (White-label, no TownHall brand)
CSV exportβŒβœ…βœ…
AnalyticsBasic (question count)Detailed (votes, timeline, answered %)Full (all metrics + export)
Session replayβœ… (read-only)βœ…βœ…
Blocked keywordsβŒβœ… (up to 20)βœ… (unlimited)
Team seats115 (extra seats $10/mo)
AI ModerationβŒβŒβœ… (GPT-4o-mini flagging)
API accessβŒβŒβœ… (CRUD sessions + export)
SupportEmail (72h)Email (24h)Priority (4h) + Slack

Annual pricing: Pro $190/yr (save $38), Business $390/yr (save $78) Free trial: 14 days full-featured Pro (no credit card required)


πŸ“’ Go-to-Market Strategy

Pre-Launch (Days 1–7)

  1. Build in public β€” Tweet thread on X (@TownHallHQ) showing the session creation flow, anonymous Q&A, and real-time voting. Share a screen recording of the organizer marking questions answered live.
  2. Product Hunt Teaser β€” Create pre-launch page with a GIF of the real-time voting flow. Share on Indie Hackers.
  3. Personal outreach β€” DM 20 engineering managers and meetup organizers offering free lifetime Pro in exchange for running their next all-hands with TownHall and providing feedback.

Launch (Day 7–14)

  1. Product Hunt launch β€” Post with the hook: "Anonymous Q&A for your meetings. No sign-up. No app install. One link." Offer 30% off first 3 months for launch week.
  2. Indie Hackers post β€” "I built a real-time anonymous Q&A tool in 3 days β€” here's how it works with Supabase Realtime." Technical deep-dive that appeals to the IH audience.
  3. Community launches β€” Post in r/SaaS, r/startups, r/programming. Target: "Ever hosted a Q&A where nobody asked anything? Here's the fix."

Post-Launch (Weeks 2–4)

  1. Content marketing β€” Blog posts: "How to Run a Better All-Hands Meeting", "The Case for Anonymous Q&A in Remote Teams", "5 Ways to Get Your Team Asking Questions."
  2. Meetup sponsorship β€” Offer free Business tier to 10 popular meetup groups (React Brussels, Python London, etc.) in exchange for using TownHall and providing a testimonial.
  3. Conference outreach β€” Discounted event plans for conference organizers. Pay-per-event: $5/session for up to 500 participants.
  4. Open source communities β€” Free unlimited sessions for open-source projects and their community calls.

Growth Channels

ChannelStrategyCost
Product HuntLaunch with demo + launch discountFree
Indie HackersBuild in public threads + technical deep-diveFree
Dev communitiesReddit r/SaaS, r/startups, Hacker NewsFree
Meetup.comDirect outreach to meetup organizersFree (manual)
Content marketing"Anonymous Q&A" SEO + meeting best practices$0 (long-tail)
Conference partnershipsDiscounted/white-label for eventsLow (time)
Embeddable widget"Add Q&A to your conference page" β€” embed snippetViral loop

πŸ“Š Success Metrics

MetricTarget (90 days)Why It Matters
Total signups400+Top-of-funnel health
Sessions created1,000+Core engagement β€” are people actually using it?
Questions submitted10,000+Is the anonymous format working? Volume signal.
Paid conversions6–8% free β†’ paidPricing + product-market fit signal
MRR$1,200–$2,500Viability as sustainable micro-SaaS
Active session rate> 40% of created sessions go liveAre sessions getting shared or abandoned?
Questions per session> 8 avgQuality signal β€” sessions with real participation
Churn (monthly)< 6%Retention health
Vote-to-question ratio> 3:1Engagement depth β€” people are voting, not just lurking
Time to first question< 30s from session openUX optimization metric β€” zero friction test
NPS> 40Word-of-mouth growth potential

⚠️ Risks & Mitigations

RiskSeverityLikelihoodMitigation
Spam/abuse (bots submit hundreds of questions)HighMediumUpstash rate limiting (5/min per IP); moderation queue on Pro; blocked keywords; report feature
Vote manipulation (organizer or participant rigs voting)MediumLowIP + participant_id rate limiting; one vote per participant per question; admin can reset votes
Low engagement (people still don't ask questions)HighMediumAnonymous-by-default lowers barrier; session start notification emails; "Be the first to ask!" nudges; gamification (top-voted question gets shoutout)
Realtime scaling (500+ concurrent participants overwhelm WebSocket)MediumMediumSupabase Realtime supports 500 concurrent per project; can scale to dedicated Realtime server or use Ably as fallback
Enterprise competitor (Slido/Mentimeter launches free tier)HighLowFocus on indie/community market where enterprise won't compete; differentiate on zero-setup + white-label
PII leakage (participant IP or data exposed)HighLowNo PII stored; participant_id is random UUID; no IP logging in question/vote tables; CSP headers
Session hijacking (someone shares the organizer link)MediumLowOrganizer link is different from participant link; session settings page requires auth; ability to regenerate participant links
Duplicate questions fatigue (same question asked 10 times)MediumMediumDuplicate detection (same text within 30s β†’ auto-merge suggestion); upvote existing instead of re-asking
Low conversion to paidHighMediumStrong free tier builds habit; watermark/branding removal is key conversion driver; 14-day Pro trial
Technical complexity of Realtime syncMediumLowSupabase Realtime handles WebSocket management; fallback to polling if WebSocket fails

🏁 Competitors

CompetitorStrengthsWeaknessesHow TownHall Wins
SlidoPolished UI, enterprise integrations (Zoom, Teams), polling$124+/mo, heavy feature set, overkill for small teamsPrice ($19 vs $124), no-account participation, simpler UX
MentimeterBeautiful presentations, live polls, word clouds$84+/mo, presentation-first, not Q&A-nativePurpose-built for Q&A, anonymous-by-default, real-time voting focus
Google Docs / NotionFree, everyone has itNot real-time, no voting, no anonymity, chaoticReal-time voting, anonymous submission, structured moderation
Zoom Chat / Slack ThreadsAlready in use, low frictionNot anonymous, no voting, gets buried in noiseAnonymous voting, persistent session, organized feed
Poll EverywhereEnterprise trust, multiple question types$120+/mo, complex setup, not anonymous-firstZero-setup, anonymous-first design, lower price
Tally / TypeformForm builders, customizationNot real-time, no live voting, no RealtimeReal-time WebSocket voting, live feed, session-based
Discord Q&A threadsFree, community already thereNot anonymous, no voting, chaotic threadingAnonymous voting, sorted by consensus, session scoping

TownHall's Moat

  1. Zero friction for participants β€” No app install, no sign-up, no email. Open link β†’ ask question. This is the single biggest barrier competitors don't solve.
  2. Real-time voting via WebSocket β€” Supabase Realtime makes vote propagation feel instant. No polling, no refresh. Competitors require page reloads or have noticeable lag.
  3. Price-to-value ratio β€” $19/mo for a tool that replaces $84-$124/mo competitors, purpose-built for the indie/community market.
  4. Designed for recurring use β€” Weekly standups, monthly all-hands, recurring meetups. Not just one-off conferences. The dashboard shows session history, analytics across sessions, and weekly digests build the habit.
  5. Moderation-first β€” Enterprise tools hide moderation behind admin panels. TownHall puts it front and center: approve, reject, pin, block keywords β€” all from the live board.

πŸ“Š Brainstorm Review Score

Review Date: 2026-07-16

MetricScoreRationale
Feasibility (1-5)1The PRD claims 3 days but includes full SaaS billing with 3 tiers (Free/Pro/Business), Stripe Checkout + Customer Portal + webhooks, background job orchestration via Inngest (4+ job types), real-time WebSocket infrastructure via Supabase Realtime, 3+ OAuth/integration flows (Supabase Auth with Google + GitHub), 9+ external services (Supabase, Upstash, Inngest, Stripe, Resend, Sentry, PostHog, Vercel), 7+ database tables with foreign keys, and complex moderation, analytics, export, replay features. Real implementation time: 8+ days minimum for a polished MVP. Hard rule violations: (1) Full billing + multi-tenant + customer-portal, (2) Background-job orchestration as core dependency, (3) 3+ OAuth flows.
Market Fit (1-5)5Strong problem: people don't ask questions in large meetings due to fear/judgment. Clear target users: engineering managers, meetup organizers, conference speakers. Wedge against competitors: zero-friction for participants (no account, no email), real-time voting, $19/mo vs $84-124/mo. Multiple viable monetization paths (per-event, subscriptions). The market fit is excellent - this is a real pain point with paid incumbents.
Complexity (1-5)1Too complex for 2-3 days: real-time sync, background jobs, multiple integrations, billing infrastructure. Requires implementing and debugging WebSocket subscriptions, rate limiting, Stripe webhooks, email workflows. The moderation, analytics, export, replay features alone are multiple days of work. Even with Supabase abstracting some infrastructure, this is a full-stack SaaS product.

Total Score: 7/15

Status: ⏸️ Deferred - Total < 12

Rationale: Strong market fit (5/5) but needs ~80% scope reduction. Rescope path: (A) client-side only (no DB, no auth, no backend) β€” localStorage + export to self-contained HTML file for session history, (B) single-user minimal server (no billing, no background jobs, no email, simplified analytics - just vote counts and question stats in PostgreSQL), or (C) "TownHall Lite" β€” anonymous Q&A board with real-time voting via Supabase Realtime, but no moderation, no analytics, no export, no replay, no billing. The core value prop (anonymous + real-time voting) can be preserved with a radically simplified MVP.