Skip to content

Repository files navigation

Quorum.ai — Collective Action for Your Neighborhood

"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.


The Problem

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

What I Built

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.


Technical Highlight: Zero-Cost AI

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.


Tech Stack

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

What Works Right Now

  • 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 DEFINER functions with search_path hardening

Architecture Notes

Assurance Contract Logic Lives in Postgres

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;

RLS Is the Real Authorization Layer

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.

Why No Middleware for Data Fetching

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.


Setup

1. Prerequisites

  • Node.js 18+
  • A Supabase project (free tier is fine)

2. Install

git clone <repo-url>
cd Quorom
npm install

3. Environment variables

cp .env.example .env.local

Fill 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:3000

4. Run migrations in Supabase SQL editor

Run in order:

  1. supabase/migrations/001_initial_schema.sql — enums, tables, handle_new_user trigger
  2. supabase/migrations/002_rls_policies.sql — RLS policies for all 5 tables
  3. supabase/migrations/003_functions_views.sql — RPCs: get_campaign_with_counts, list_active_campaigns, recompute_campaign_status, get_my_pledged_campaigns

5. Configure Supabase Auth

  • Authentication → Providers → Email: disable "Confirm email" for dev (or increase rate limits)
  • Authentication → URL Configuration: set Site URL to http://localhost:3000, add http://localhost:3000/auth/callback to Redirect URLs

6. Seed demo data (optional)

-- 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 editor

7. Run

npm run dev
# → http://localhost:3000

Project Structure

app/
  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.)

What's Next (If I Keep Building)

  • 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_status for 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.

License

MIT — build on top of this, fork it, make it work for your community.

About

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages