"I'm in if at least 20 others join by Friday."
I built Quorum.ai because I kept watching good neighborhood ideas die the same quiet death — someone posts in the group chat, a few people like it, nobody wants to be the first to actually commit, and the idea disappears. Not because it was bad. Because of a coordination problem.
The economics of collective action have a name for this: the assurance contract (or threshold pledge). The idea is simple and 50 years old. But nobody's built a clean, AI-assisted version of it for hyperlocal organizing. That's what this is.
Your block wants to bulk-buy solar panels. Your building wants to hire a shared snow removal service. A group of parents wants to organize a carpool. Your tenants' association wants to collectively negotiate with a landlord.
Every one of these fails because of first-mover risk — nobody wants to commit time, money, or reputation unless they know enough others will too. So the idea stalls. The chat dies. Nothing happens.
Existing tools don't help:
- Petitions — expressive, not binding
- Crowdfunding — takes real money upfront, designed for product launches
- Group chats — vibes only, no accountability
- Community boards — asynchronous and bureaucratic
Quorum.ai is a conditional-commitment engine. Organizers create campaigns with a threshold ("I need 25 neighbors") and a deadline. Participants pledge — non-bindingly — and the campaign only activates if the threshold is crossed before time runs out.
When quorum is reached, the AI Organizer (powered by Claude Sonnet via Puter.js) kicks in and handles all the coordination labor:
- Drafts a personalized outreach message to send to your network
- Researches and compares local vendors for the specific task
- Splits the cost fairly across all committed participants
- Generates a plain-language participation agreement
If quorum isn't reached, every pledge silently expires. No awkward follow-ups, no commitment, no money collected.
Nothing is collected through this platform. Pledges are non-binding. Payments happen offline after quorum. Escrow and identity verification are explicitly out of scope for this version — clean extension points are left.
The most interesting technical choice here was the AI layer. I didn't want to expose my own API keys or eat compute costs at scale. Puter.js solves this with a "user-pays" model — the AI runs client-side, authenticated against each user's own free Puter account. As the developer, I pay $0. Users get a generous free tier.
This meant the AI Organizer had to be architecturally client-side-only, which shaped how I built it. All Puter calls are gated behind a usePuter() hook that detects whether the script loaded, distinguishes SSL/cert errors from timeouts, and surfaces specific error messages. The response parser handles both flat strings and the raw Claude API's {type, text} content block array shape.
| Layer | Choice | Why |
|---|---|---|
| Framework | Next.js 16 (App Router) + TypeScript | Server components for SEO, client components for real-time UI |
| Styling | Tailwind CSS v4 | CSS-native variables, @theme inline for design tokens |
| Animation | Framer Motion | Spring physics, useReducedMotion, layout animations |
| Database + Auth | Supabase (Postgres + RLS) | Row-level security, magic link + email/password auth, Postgres RPCs |
| AI | Puter.js → Claude Sonnet 4.6 | Zero developer cost, user-pays model, runs entirely client-side |
- Landing page — full animated marketing page with hero, problem/solution, how-it-works, use cases bento, AI Organizer explainer, FAQ
- Auth — email/password sign-up and sign-in with friendly error handling and email verification flow
- Onboarding — display name + neighborhood area, required before accessing the app
- Dashboard — filterable campaign grid with live pledge counts
- Campaign creation — full form with category, cost model, threshold, deadline
- Campaign detail — pledge/withdraw with optimistic UI; confetti on quorum crossing; vendor table; cost split display; AI agreement text; comments
- My Activity — all your pledges + all campaigns you've created
- AI Organizer (creator-only, appears after quorum) — outreach draft, vendor research, cost split, participation agreement — all saved to DB, visible to pledgers
- SEO — dynamic OG images per campaign, sitemap, robots.txt, Organization + Event JSON-LD, full Twitter card support
- Security — HTTP headers (CSP, X-Frame-Options, HSTS-ready), open redirect protection, RLS on all 5 tables,
SECURITY DEFINERfunctions withsearch_pathhardening
Campaign status transitions (active → met, active → failed) happen server-side in a recompute_campaign_status RPC. It's called on every pledge change and on every campaign view. No client-side status logic. This means even if a user has a stale tab open, they always see fresh state.
-- The entire threshold check in ~10 lines
IF v_count >= v_campaign.threshold_count THEN
v_new_status := 'met';
ELSIF NOW() > v_campaign.deadline AND v_campaign.status = 'active' THEN
v_new_status := 'failed';
END IF;Every table has RLS enabled. The campaign detail page does a creator check client-side to show/hide the AI Organizer panel, but even if someone bypassed that check, the underlying Supabase mutations (UPDATE campaigns SET ai_outreach_text...) would be denied by the campaigns_update_own policy. Client checks are UX; RLS is enforcement.
The campaign detail page is a client component (needs useState for optimistic pledge updates, confetti, real-time counts). Metadata and JSON-LD live in a separate layout.tsx at the route segment level — a server component that fetches the campaign once, server-side, for SEO purposes. The page then fetches again client-side for the interactive layer. Slightly redundant, but clean separation of concerns.
- Node.js 18+
- A Supabase project (free tier is fine)
git clone <repo-url>
cd Quorom
npm installcp .env.example .env.localFill in .env.local:
NEXT_PUBLIC_SUPABASE_URL=https://your-project-ref.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here
NEXT_PUBLIC_APP_URL=http://localhost:3000Run in order:
supabase/migrations/001_initial_schema.sql— enums, tables,handle_new_usertriggersupabase/migrations/002_rls_policies.sql— RLS policies for all 5 tablessupabase/migrations/003_functions_views.sql— RPCs:get_campaign_with_counts,list_active_campaigns,recompute_campaign_status,get_my_pledged_campaigns
- Authentication → Providers → Email: disable "Confirm email" for dev (or increase rate limits)
- Authentication → URL Configuration: set Site URL to
http://localhost:3000, addhttp://localhost:3000/auth/callbackto Redirect URLs
-- Edit supabase/seed.sql: replace 'YOUR_USER_UUID_HERE' with your actual UUID
-- (find it in Supabase → Authentication → Users after signing up)
-- Then run the file in the SQL editornpm run dev
# → http://localhost:3000app/
page.tsx # Landing page
auth/ # Sign in / sign up
auth/callback/ # Supabase auth callback
onboarding/ # First-time setup
dashboard/ # Campaign discovery
campaigns/new/ # Create a campaign
campaigns/[id]/ # Campaign detail + pledge + AI
layout.tsx # generateMetadata + Event JSON-LD
opengraph-image.tsx # Dynamic per-campaign OG image
my/ # Personal activity
opengraph-image.tsx # Root branded OG image
sitemap.ts # Dynamic sitemap from DB
robots.ts # Crawl rules
manifest.ts # PWA manifest
components/
ui/ # Button, Badge, Input, Skeleton, Select
layout/navbar.tsx # Sticky nav with mobile drawer
logo.tsx # Logo component (PNG wordmark)
campaign-card.tsx # Animated card with optimistic state
pledge-button.tsx # Pledge / withdraw toggle
progress-bar.tsx # Spring-animated count-up bar
ai-organizer.tsx # 4-panel AI feature suite (creator-only)
confetti.tsx # Quorum celebration
lib/
ai.ts # Puter.js wrapper (aiChat, aiChatJSON, waitForPuter)
types.ts # DB model types
utils.ts # formatDeadline, formatCurrency, cn, etc.
supabase/
client.ts # Browser client
server.ts # Server client (cookies)
hooks/
use-auth.ts # Supabase auth subscription
use-puter.ts # Puter.js load state (loading/ready/cert-error/unavailable)
supabase/
migrations/ # Run in order: 001 → 002 → 003
seed.sql # Demo campaigns + synthetic organizer
middleware.ts # Route protection + onboarding redirect
next.config.ts # Security headers (CSP, X-Frame-Options, etc.)
- Stripe escrow — collect money only when quorum is met, release to vendor on completion. This is the natural monetization point (small platform fee on disbursement).
- Neighbor verification — verify that pledgers actually live in the area. Could use address confirmation + postcard, or leverage existing services.
- Realtime pledge count — Supabase Realtime subscription so the progress bar moves live as people pledge.
- Scheduled deadline processing — a Supabase Edge Function on cron to call
recompute_campaign_statusfor all active campaigns nearing their deadline. - Push notifications — nudge pledgers when the campaign is 80% of the way to quorum.
- Dark mode — CSS variables are already set up in
globals.css, just needs a theme toggle and dark values. - Mobile app — the PWA manifest is already there; a React Native wrapper would be straightforward.
MIT — build on top of this, fork it, make it work for your community.