Feature Voting Board
Public roadmap + feature voting for indie SaaS. Flat pricing, embeddable widget, AI dedup. Undercuts Canny. Weekend MVP guide + prompts.
A SaaS founder gets feature requests in every channel at once. Twitter DMs. Intercom. Slack Connect threads. Email forwards from the CEO. Postmortem docs from customer interviews. One engineer maintains a Notion page called "Roadmap Ideas (actual)" that nobody trusts. By the time the team picks a quarterly theme, half the requests have gone stale and the other half were from one loud user who never actually upgrades. The incumbent answer is Canny at $360/month plus per-member pricing. That is a rounding error for a Series A company and a deal-breaker for the indie SaaS at $3K MRR. Founders who cannot justify Canny's price end up hand-building a Trello board, inheriting a dusty UserVoice subscription, or maintaining a GitHub issues repo labeled "public." None of these surface signal.
Built for Developers, Solo Founders.
Suggested stack: Next.js 14 App Router, Supabase, Claude Haiku 4.5 for dedup, Upstash Redis rate limits, Resend + custom domain, Stripe Billing + Vercel. Weekend scope: about 8 hours.
The Problem
A SaaS founder gets feature requests in every channel at once. Twitter DMs. Intercom. Slack Connect threads. Email forwards from the CEO. Postmortem docs from customer interviews.…
The Solution
A multi-tenant feature-voting SaaS priced for the indie and small-team segment. Each customer gets a public board at their own subdomain (or custom domain on paid tiers), an…
Market Research
Canny has 1,000+ paying teams (public ARR estimates ~$10M+). It was acquired by Permanent Capital in 2024 — a clear exit validation of the category at scale. Their pricing at…
Competitive Landscape
Canny — Category leader for SaaS feature voting. Deep Intercom/Slack integrations, polished admin. Acquired by Permanent Capital 2024. Per-user-per-month pricing adds fast; custom…
Business Model
Free ($0) — 1 board, 100 total submissions, subdomain only, “Powered by” footer
Recommended Tech Stack
Next.js 14 App Router — Middleware routes `tenantSlug.product.com` or custom domain to `/[tenantSlug]/board`. Server Actions for vote/submit. Public board pages as Server…
AI Prompts to Build This
Copy these build prompts into Claude, Cursor, or your AI coding tool. Create a free account to unlock the full research behind them.
1. Multi-Tenant Scaffold
Create a Next.js 14 App Router multi-tenant SaaS for a feature voting board. Supabase backend, Tailwind, TypeScript. Schema (Supabase Postgres): - tenants(id, slug, custom_domain, owner_user_id, brand_primary_hex, stripe_customer_id, plan ENUM[free,pro,team], created_at) - features(id, tenant_id, title, body, status ENUM[under_review,planned,in_progress,shipped,wontfix], vote_count INT, created_by_email, created_at, shipped_at) - votes(id, feature_id, voter_email, voter_ip, created_at) — unique(feature_id, voter_email) - comments(id, feature_id, body, author_email, author_role ENUM[public,admin], created_at) - subscribers(id, feature_id, email) — voters who want status update emails RLS: tenants own-row; features/votes/comments scoped by tenant_id; public read on non-deleted rows. Middleware (Next.js): - Inspect request Host. If host matches tenants.custom_domain OR {slug}.portal.product.com, rewrite to /[tenantSlug]/board/... - If host is app.product.com, render admin routes at /admin/... Routes: - /[tenantSlug]/board (public list, filter by status, sort by votes|newest) - /[tenantSlug]/f/[featureId] (detail + comments) - /admin/login - /admin/features (triage view: new submissions, pending dedup, status changes) - /admin/settings (domain, branding, billing, integrations) Use Server Actions for the /vote and /submit mutations; include revalidatePath on the public board.2. AI Dedup + Rate Limiting
Implement AI-powered duplicate detection and vote spam protection. This is the product's differentiation vs Canny/Nolt. Dedup flow on submission: 1. User submits new feature via /submit Server Action. 2. Run Postgres FTS against existing features for this tenant: SELECT id, title, body, vote_count, ts_rank(to_tsvector(title || ' ' || body), websearch_to_tsquery($query)) AS rank FROM features WHERE tenant_id=$tid AND status NOT IN ('shipped','wontfix') ORDER BY rank DESC LIMIT 10; 3. If top candidate rank > 0.3, pass the submission + top 5 candidates to Claude Haiku 4.5: System prompt: "You identify whether a new feature request is a duplicate of existing ones. Return JSON: {is_duplicate: bool, match_id: string|null, confidence: 0-1, reason: string}" 4. If is_duplicate AND confidence > 0.7: show user the modal: "This looks similar to [existing feature]. Vote for it instead?" with [Vote existing] or [Submit anyway]. 5. If user confirms submit anyway, create new feature. Track the "declined dedup" rate as a product metric. Rate limiting via Upstash Ratelimit: - Votes: 1 per (feature_id, voter_email) — DB unique constraint. - Submissions: 20/day per tenant (stop spam) and 5/hour per voter_email. - IP-based: 100 requests/hour per IP per tenant to the /vote and /submit endpoints. - Return 429 with Retry-After header when exceeded. Cost: Haiku dedup costs ~$0.002 per submission; budget-safe even at 10K submissions/month ($20).3. Embeddable Widget + Status Notifications
Build the embeddable widget and the status-change notification loop. These close the retention flywheel. Embeddable widget: - Host a standalone JS bundle at https://cdn.product.com/widget.js. Customer pastes: - On load, create a floating button ("💡 Feedback") in bottom-right. - Click opens an iframe pointing to https://acme.portal.product.com/widget (sandboxed with allow="forms"). - Inside the iframe, render a trimmed version of the board: list top 10 active features, submit button, search. - postMessage bridge: if the host app wants to pass user_email (for pre-auth), the parent calls window.postMessage({type: 'identify', email: '...'}, '*') and the widget iframe listens. Reduces submission friction to one click. - Widget HTML/CSS: Tailwind inlined, 50KB total, zero dependencies on host app. Status-change notifications: 1. Admin changes feature status in /admin/features. Server Action writes new status + records the prior one. 2. Enqueue a background job (Vercel Cron or Upstash QStash): "send status-change emails for feature_id=X." 3. Job queries all (voters + subscribers) for that feature, dedupes emails. 4. Send via Resend using tenant's sender domain (Team tier) or platform domain (Free/Pro). React Email template with the feature title, new status, tenant branding. 5. Track open + click via Resend webhooks; store as analytics.status_email_opens for the admin dashboard. 6. For shipped status: auto-generate a changelog entry at /[tenantSlug]/changelog with markdown body. Team tier can toggle "auto-publish to changelog." This is the retention flywheel: voters get re-engaged on every ship event, admins get free proof-of-shipping marketing, and dormant accounts reactivate when a feature they cared about goes live.