Feature spec
IntermediateCategory
vertical-tools
Build with AI
4–8 hours with Lovable or V0
Custom build
1–2 weeks custom dev
Running cost
$0/mo up to ~500 users; $25–75/mo at scale
Works on
Everything it takes to ship Crowdsourced Event Planning — parts, prompts, and real costs.
Crowdsourced event planning lets community members propose events, upvote them to the top, vote on the best date, and comment — with admins approving the winners into your events calendar. You need Supabase for votes, proposals, and date polls; Supabase Realtime for live vote count updates; and a Resend Edge Function for approval emails. With Lovable or V0 you can ship in 4–8 hours. Running costs start at $0/month on Supabase's free tier and reach $50–75/month at 10,000 users.
What Crowdsourced Event Planning Actually Is
Crowdsourced event planning turns your community into co-creators: members propose event ideas, vote on which ones should happen, pick the best date from a Doodle-style poll, and comment on the details — while admins retain final approval before the event appears on the public calendar. The core product decisions are how to surface the best proposals (a time-decay hot score beats pure vote count), how to prevent vote manipulation (database unique constraints, not client-side logic), and how tightly to couple the admin approval workflow to your existing events system. The result is higher attendance and lower organizer burnout, because the community self-selects events people actually want.
What users consider table stakes in 2026
- Proposal feed sorted by a hot score that weights recent votes over old ones — pure vote count lets early proposals dominate forever
- One-click upvote and downvote with instant optimistic UI update — users expect vote feedback in under 100ms
- Doodle-style date poll where multiple dates can be selected per voter, with a clear 'most available' winner highlighted
- Nested comment threads (at least 2 levels) with live updates when new comments are posted
- Status badges on proposals — Proposed, Voting, Approved, Rejected — so the community can track progress
- Admin moderation controls: approve, reject, pin to top, or request changes on any proposal
Anatomy of the Feature
Six components. The voting and hot-score systems are where builds fail — AI tools default to client-side logic that is both slow and manipulatable.
Event proposal feed
UIshadcn/ui Card list with infinite scroll powered by Tanstack Query's useInfiniteQuery. Each card shows the proposal title, proposer avatar and display name, vote score, comment count, date poll status, and a status badge. The feed is sorted by a hot score computed in Postgres — never in the React component.
Note: Tanstack Query's staleTime of 30 seconds prevents overfetching on scroll while Supabase Realtime pushes vote count deltas instantly.
Voting system
BackendSupabase votes table with columns (user_id, proposal_id, value CHECK IN (-1, 1)) and a UNIQUE constraint on (user_id, proposal_id). Vote total is computed via a Postgres generated column or a materialized view refreshed on each vote INSERT/DELETE. Supabase Realtime broadcasts the updated vote_score to all subscribers on the proposals channel.
Note: Never compute the hot score in React — a Postgres generated column using the formula votes / (EXTRACT(EPOCH FROM (NOW() - created_at)) / 3600 + 2)^1.5 is the correct approach.
Date polling system
DataProposer submits 3–5 date options stored in date_poll_options. Attendees respond per option (is_available bool) in date_poll_responses with a UNIQUE constraint on (user_id, option_id). The winning date is the option with the highest count of is_available = true responses. Supabase Realtime subscription pushes response count updates live.
Note: Display date options in the voter's local timezone using date-fns-tz — proposals often span timezones in distributed communities.
Comment threads
BackendSupabase comments table with a self-referential parent_id FK for 2-level nesting. Fetch parent comments and their immediate children in two queries — PostgREST does not support recursive CTEs from the JS client, so do not attempt app-level recursion. react-markdown renders safe markdown in comment content. Supabase Realtime pushes new comments to the open proposal.
Note: Limit nesting to 2 levels by setting parent_id to the root comment's id on any deeper reply — this avoids recursive query complexity entirely.
Admin moderation system
BackendAdmin role stored as a Supabase custom JWT claim in app_metadata.role = 'admin'. RLS policies check auth.jwt() -> 'app_metadata' ->> 'role' = 'admin' for UPDATE on proposal status. Admin panel shows all proposals with filter tabs (Proposed, Voting, Approved, Rejected). Approval triggers a Supabase Edge Function that creates an entry in the events table and sends a Resend email to everyone who voted.
Note: Never store admin status in a client-readable users table column — always use JWT claims checked in RLS. A malicious user who updates is_admin = true in the client gets nothing without the claim.
Notification system
BackendSupabase Edge Function triggered by admin approval status change sends Resend emails to all voters on that proposal, including the proposer. Additional Edge Function sends email when a comment is posted on a proposal you created or voted on. Emails contain direct links back to the proposal page.
Note: Batch voter email lookup with a single SELECT user_id FROM votes WHERE proposal_id = $1 and pass to Resend's batch send API — do not loop individual sends.
The data model
Run this in the Supabase SQL editor. The hot score as a generated column means sorting is always fast and cannot be manipulated client-side. The UNIQUE constraints prevent double-voting at the database level.
1create table public.event_proposals (2 id uuid primary key default gen_random_uuid(),3 created_by uuid references auth.users(id) on delete cascade not null,4 title text not null,5 description text,6 location text,7 category text,8 status text not null default 'proposed' check (status in ('proposed','voting','approved','rejected')),9 vote_score int not null default 0,10 hot_score numeric generated always as (11 vote_score::numeric / power(12 (extract(epoch from (now() - created_at)) / 3600.0) + 2,13 1.514 )15 ) stored,16 created_at timestamptz not null default now()17);1819create table public.votes (20 id uuid primary key default gen_random_uuid(),21 user_id uuid references auth.users(id) on delete cascade not null,22 proposal_id uuid references public.event_proposals(id) on delete cascade not null,23 value int not null check (value in (-1, 1)),24 created_at timestamptz not null default now(),25 unique(user_id, proposal_id)26);2728create table public.date_poll_options (29 id uuid primary key default gen_random_uuid(),30 proposal_id uuid references public.event_proposals(id) on delete cascade not null,31 date_option date not null,32 created_at timestamptz not null default now()33);3435create table public.date_poll_responses (36 id uuid primary key default gen_random_uuid(),37 user_id uuid references auth.users(id) on delete cascade not null,38 option_id uuid references public.date_poll_options(id) on delete cascade not null,39 is_available bool not null,40 created_at timestamptz not null default now(),41 unique(user_id, option_id)42);4344create table public.comments (45 id uuid primary key default gen_random_uuid(),46 proposal_id uuid references public.event_proposals(id) on delete cascade not null,47 user_id uuid references auth.users(id) on delete cascade not null,48 parent_id uuid references public.comments(id) on delete cascade,49 content text not null,50 created_at timestamptz not null default now()51);5253-- RLS54alter table public.event_proposals enable row level security;55alter table public.votes enable row level security;56alter table public.date_poll_options enable row level security;57alter table public.date_poll_responses enable row level security;58alter table public.comments enable row level security;5960-- Proposals: anyone authenticated can read; authenticated users can insert own; admins can update status61create policy "Authenticated users can read proposals"62 on public.event_proposals for select63 using (auth.uid() is not null);6465create policy "Authenticated users can propose"66 on public.event_proposals for insert67 with check (auth.uid() = created_by);6869create policy "Admins can update proposal status"70 on public.event_proposals for update71 using (72 (auth.jwt() -> 'app_metadata' ->> 'role') = 'admin'73 or auth.uid() = created_by74 );7576-- Votes: authenticated users can vote; unique constraint prevents double-vote77create policy "Authenticated users can vote"78 on public.votes for insert79 with check (auth.uid() = user_id and auth.uid() is not null);8081create policy "Users can remove own votes"82 on public.votes for delete83 using (auth.uid() = user_id);8485create policy "Votes are readable by authenticated users"86 on public.votes for select87 using (auth.uid() is not null);8889-- Date poll options and responses: readable by all authenticated90create policy "Authenticated users can read poll options"91 on public.date_poll_options for select92 using (auth.uid() is not null);9394create policy "Proposal owner can add date options"95 on public.date_poll_options for insert96 with check (97 auth.uid() = (select created_by from public.event_proposals where id = proposal_id)98 );99100create policy "Authenticated users can respond to polls"101 on public.date_poll_responses for all102 using (auth.uid() = user_id)103 with check (auth.uid() = user_id);104105-- Comments: readable by authenticated; writeable by authenticated106create policy "Authenticated users can read comments"107 on public.comments for select108 using (auth.uid() is not null);109110create policy "Authenticated users can comment"111 on public.comments for insert112 with check (auth.uid() = user_id);113114-- Trigger to update vote_score on votes insert/delete115create or replace function public.update_proposal_vote_score()116returns trigger language plpgsql security definer as $$117begin118 if tg_op = 'INSERT' then119 update public.event_proposals120 set vote_score = vote_score + new.value121 where id = new.proposal_id;122 elsif tg_op = 'DELETE' then123 update public.event_proposals124 set vote_score = vote_score - old.value125 where id = old.proposal_id;126 end if;127 return null;128end;129$$;130131create trigger votes_update_score132 after insert or delete on public.votes133 for each row execute function public.update_proposal_vote_score();134135-- Indexes136create index proposals_hot_score_idx on public.event_proposals (hot_score desc);137create index proposals_status_idx on public.event_proposals (status);138create index comments_proposal_parent_idx on public.comments (proposal_id, parent_id);Heads up: The hot_score generated column and its index mean ORDER BY hot_score DESC on the feed query never triggers a sort — it uses the index. The trigger keeps vote_score in sync atomically with every vote insert or delete.
Build it — pick your path
Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.
Lovable handles Supabase Realtime voting feed and nested comments well; infinite scroll and the hot-score sorting algorithm need precise specification in the prompt to generate correctly.
Step by step
- 1Create a new Lovable project, connect Lovable Cloud to provision Supabase, and confirm Auth is enabled in the Cloud tab
- 2Paste the prompt below and let Agent Mode build the proposal feed, proposal creation form, voting buttons, date poll UI, comment threads, and admin moderation panel as separate pages or components
- 3After generation, open the Cloud tab → Database and run the vote_score trigger SQL manually if Lovable did not generate it — the trigger is easy to miss
- 4Test voting with two different browser sessions to verify the unique constraint blocks double votes
- 5Set up admin JWT claims in the Supabase Dashboard under Authentication → Users → select user → Edit → App Metadata: add { "role": "admin" }
Build a crowdsourced event planning feature. Community members propose events and vote on them. Database tables in Supabase: event_proposals (id, created_by FK auth.users, title, description, location, category, status CHECK IN ('proposed','voting','approved','rejected'), vote_score int default 0, created_at); votes (user_id FK, proposal_id FK, value CHECK IN (-1, 1), UNIQUE(user_id, proposal_id)); date_poll_options (id, proposal_id FK, date_option date); date_poll_responses (user_id FK, option_id FK, is_available bool, UNIQUE(user_id, option_id)); comments (id, proposal_id FK, user_id FK, parent_id FK self-ref, content, created_at). Proposal feed: shadcn/ui Card list sorted by hot_score (votes / (age_hours + 2)^1.5) — compute in Postgres, never React; infinite scroll with Tanstack Query useInfiniteQuery; show vote count, comment count, status badge. Voting: upvote/downvote buttons; enforce one vote per user with UNIQUE DB constraint; optimistic UI update; cancel vote by clicking same button again (DELETE from votes). Date poll: proposer can add 3-5 date options; voters check which dates they're available; show which date has most availability. Comments: nested 2 levels (parent + reply); react-markdown for content; Supabase Realtime for live updates. Admin panel (role check via Supabase JWT claim app_metadata.role = 'admin'): approve/reject/pin proposals; on approval create entry in Supabase events table (if it exists) and send Resend email to all voters. Handle edge cases: user voting on own proposal (allow but note it); proposal with no date options still valid; tie in date poll shows multiple winners; empty state when no proposals; 'load more' vs infinite scroll. Supabase Realtime: subscribe to single proposals channel for vote count updates — do not subscribe per proposal row.Where this path bites
- Complex nested comment rendering with Realtime updates often degrades across multiple Lovable correction prompts — specify 2-level nesting and two separate queries explicitly
- The hot-score formula (time-decay sort) must be spelled out exactly in the prompt; Lovable defaults to ORDER BY vote_score which lets old proposals dominate
- Lovable Cloud's managed Supabase does not expose the SQL editor in the Supabase Dashboard — use the Cloud tab SQL runner for manual schema adjustments
Third-party services you'll need
Crowdsourced event planning needs only two external services — Supabase handles the heavy lifting.
| Service | What it does | Free tier | Paid from |
|---|---|---|---|
| Supabase | Database (proposals, votes, polls, comments), Auth, Realtime for live vote updates, Edge Functions for approval emails | 2 projects, 500MB DB, 2M Realtime messages/month | Pro $25/mo |
| Resend | Email notifications when proposals are approved, when a voted proposal gets new comments, and when date polls close | 3,000 emails/month | $20/mo for 50,000 emails (approx) |
Swipe the table sideways to see pricing.
What it costs to run
Drag through the tiers to see how your monthly bill scales with users — no surprises later.
Estimated monthly running cost
Supabase free tier (500MB DB, 2M Realtime messages) handles low proposal and vote volume easily. Resend free tier covers approval notification emails.
Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.
What breaks when AI tools build this
The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.
Vote manipulation via logout-and-revote
Symptom: AI tools often generate the vote uniqueness check client-side — checking if the user's vote already exists in local state before calling Supabase. A user who logs out and signs in with a different account, or who clears browser storage, bypasses this check entirely. The unique constraint on (user_id, proposal_id) in the database is the only defense that actually holds.
Fix: Require authenticated users for all votes (RLS policy: auth.uid() IS NOT NULL on votes INSERT). Rely entirely on the UNIQUE(user_id, proposal_id) database constraint for double-vote prevention — the client should only provide optimistic UI feedback, never enforce the rule.
Hot score computed in React instead of Postgres
Symptom: The default AI output sorts proposals by vote_score DESC, which means the oldest popular proposals stay at the top forever. When AI generates the time-decay formula at all, it often places it in the React sort function — this means every client computes a slightly different score based on their local clock, and the sort order flickers on re-render.
Fix: Compute hot_score as a Postgres generated column using the formula: vote_score / POWER((EXTRACT(EPOCH FROM (NOW() - created_at)) / 3600.0) + 2, 1.5). Index on hot_score DESC. The query always uses ORDER BY hot_score DESC — the React component never sorts.
Nested comments cause N+1 queries via recursive fetching
Symptom: When AI generates nested comments using Supabase's .select() with embedded FK relations, it often attempts to recursively fetch children in a loop — one query per comment. With 100 comments across 20 proposals, this creates 100+ sequential database round trips on a single page load.
Fix: Limit nesting to 2 levels and fetch both levels in exactly two queries: one for all root comments (WHERE parent_id IS NULL) and one for all child comments (WHERE parent_id IS NOT NULL AND proposal_id = $1). Merge and nest client-side. This is O(2) queries, not O(N).
Per-row Realtime subscriptions flood the connection pool
Symptom: AI tools frequently generate a Supabase Realtime subscription per proposal card in the feed list — if 50 proposals are visible, that is 50 simultaneous channel subscriptions. Supabase free tier allows 200 concurrent connections; a single active user with 50 subscriptions consumes 25% of the entire account's connection budget.
Fix: Subscribe to a single Realtime channel filtering by table: supabase.channel('proposals').on('postgres_changes', { event: '*', schema: 'public', table: 'event_proposals' }, callback). Update local Tanstack Query cache using queryClient.setQueryData when the channel fires.
Admin role checked client-side enables privilege escalation
Symptom: The most common admin implementation generated by AI tools stores is_admin as a column in a users table and checks it in React before showing the admin panel. A user who opens browser dev tools and modifies localStorage or React state can make the admin panel visible — and if the RLS policies also use client-readable data for authorization, they can perform admin actions.
Fix: Store admin status exclusively in Supabase app_metadata JWT claims, set server-side via the Supabase service role. RLS UPDATE policy checks (auth.jwt() -> 'app_metadata' ->> 'role') = 'admin'. The client can read the JWT claim to show/hide UI, but the database enforces the permission independently.
Best practices
Compute and index the hot score in Postgres — never sort in React; the algorithm must be consistent across all clients and impervious to clock drift
Enforce all vote uniqueness rules with database constraints, not application logic — the UNIQUE(user_id, proposal_id) index is the real lock
Subscribe to one Supabase Realtime channel per table, not per row — use the filter option within a single channel subscription to scope updates
Limit comment nesting to 2 levels and fetch parent and child comments in exactly two queries — recursive DB queries via PostgREST are fragile and slow
Use Supabase JWT claims for admin roles and check them in RLS policies — never check client-readable columns for write permissions
Send approval notification emails in a batch Resend API call rather than individual sends — one API call for 500 voter emails is vastly more reliable than 500 sequential calls
Show a 'you already voted' indicator before the user tries to vote again — this reduces confusion and eliminates unnecessary failed INSERT attempts
Convert all date poll options to UTC for storage and use date-fns-tz to display them in each voter's local timezone — distributed communities span multiple timezones
When You Need Custom Development
Lovable and V0 handle the proposal-vote-comment loop reliably. These requirements push past what AI tools can generate correctly:
- Events need venue booking and catering vendor coordination alongside community approval — this requires external vendor APIs and a multi-step workflow engine beyond a simple status column
- Approved events need a Stripe crowdfunding component — community members commit budget before the event is confirmed, with conditional charge logic on reaching a funding goal
- Proposal volume is high enough to need ML-based spam detection — integrating OpenAI moderation endpoint before proposals reach the admin queue is a non-trivial pipeline
- Integration with Eventbrite or Lu.ma is required — syncing approved proposals to external event platforms needs dedicated webhook handlers and event mapping logic
RapidDev builds these features for production
Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.
Frequently asked questions
How do I prevent the same user from voting multiple times?
Add a UNIQUE constraint on (user_id, proposal_id) in the votes table — this is a database-level lock that cannot be bypassed from the client. Require authentication before voting (RLS policy: auth.uid() IS NOT NULL on INSERT). The client can show optimistic feedback, but the unique constraint is the only defense that matters.
What is the best algorithm for surfacing popular event proposals?
Use a time-decay hot score: votes / (age_in_hours + 2)^1.5. This matches the Hacker News formula and naturally demotes old proposals as newer ones accumulate votes. Compute it as a Postgres generated column indexed with DESC — never sort in React. A pure vote-count sort lets early proposals dominate the feed indefinitely.
How do I implement a date poll like Doodle?
Create a date_poll_options table with one row per date option per proposal, and a date_poll_responses table where each user marks each option as is_available (bool) with a UNIQUE(user_id, option_id) constraint. Show availability counts per option and highlight the option with the most 'available' responses. The winning date is selected by count, not first-to-submit.
How do I handle spam or inappropriate proposals?
Two layers: first, require account authentication before submitting a proposal (RLS: auth.uid() IS NOT NULL on INSERT). Second, give admins the ability to reject proposals without explanation. If volume grows high enough to require automated pre-screening, add OpenAI moderation API check in a Supabase Edge Function on proposal INSERT — flag proposals with high toxicity scores for admin review before they appear in the feed.
Can non-logged-in users view proposals?
Yes — adjust the SELECT RLS policy on event_proposals to use USING (true) for a public feed, or USING (auth.uid() IS NOT NULL) for an authenticated-only community. Voting and commenting should always require authentication. A read-only public feed often increases sign-up conversion because visitors see the community activity before deciding to join.
How do I notify users when their proposal is approved?
Trigger a Supabase Edge Function on admin status update to 'approved'. The function queries all users who voted for that proposal (SELECT DISTINCT user_id FROM votes WHERE proposal_id = $1), fetches their emails from auth.users via the service role, and sends a single batched Resend API call. The proposer receives a separate personalized email.
Can I limit who can submit event proposals?
Yes — gate the INSERT RLS policy with additional checks. Examples: require the user to have been a member for more than 7 days (check created_at on auth.users), require a minimum vote score on previous proposals, or restrict to users with a specific role claim. The simplest production approach is authenticated-only (auth.uid() IS NOT NULL) with an admin ability to ban specific users.
How do I convert an approved proposal into a confirmed event?
In the admin approval Supabase Edge Function, run an INSERT into your events table using the proposal's title, description, location, and the winning date from the date poll. Set the proposal status to 'approved'. Then send Resend emails to all voters with a link to the new confirmed event page. This creates a clean handoff from community proposal to official calendar entry.
Need this feature production-ready?
RapidDev builds crowdsourced event planning into real apps — auth, database, payments — at $13K–$25K.
Book a free consultation30-min call. No commitment.