Nine alternate versions of yourself — Founder, Billionaire, Artist, Athlete, Monk, Scientist, Explorer, Romantic, Shadow — debate one real decision you bring them. They disagree, interrupt, change their minds, and vote.
Live: https://the-council-murex.vercel.app
- Frontend: Vite + React.
- Backend: Vercel serverless functions (
api/*.js). - Inference: Groq (
openai/gpt-oss-120b). - Text-to-Speech (TTS): OpenAI TTS (primary) / Gemini TTS (fallback).
- Storage & Persistence: Cloudflare KV (for shared results) and Supabase (for authenticated user history).
- Rate Limiting: Upstash Redis (or Cloudflare KV).
- Authentication: Clerk (with Google OAuth fallback logic).
- Analytics & Monitoring: PostHog, Hotjar, Resend, and Sentry.
git clone https://github.com/CesarNog/the-council.git
cd the-council
npm install
cp .env.example .env.local # fill in the necessary vars, see below
npm run dev # starts frontend onlyServerless functions (api/*.js) only run under vercel dev, not plain vite. To exercise the full stack locally:
npx vercel devSee .env.example for the complete list. Essential ones include:
| var | used for |
|---|---|
GROQ_API_KEY |
debate generation (api/council.js) |
CLOUDFLARE_API_TOKEN & CLOUDFLARE_ACCOUNT_ID |
KV read/write for rate limits & sharing |
SESSION_SECRET |
signs the session cookie (api/_session.js) |
OPENAI_API_KEY |
Synthesizes voices for the personas via OpenAI TTS |
GEMINI_TTS_API_KEY |
Fallback TTS synthesizer |
Optional features (Clerk Auth, Supabase, Upstash Redis, Sentry, PostHog, etc.) require their respective variables set.
npm testCovers src/lib/*.test.js and api/*.test.js. No component/UI tests yet.
Push to main — the repo is linked to Vercel, deploys are automatic. Manual deploy: npx vercel deploy --prod.
- Frontend (
src/):components.jsx: All UI including Chamber, ShareBar, Landing.auth-ui.jsx/clerk-auth-ui.jsx: Auth buttons + profile UI.lib/: Pure functions, persona definitions (personas.js), AI prompt builders, and TTS wrappers.App.jsx: Routing.
- Backend (
api/):council.js: POST — generates a debate via Groq, persists it, rate-limits.result.js: GET — fetches a persisted debate by id.profile.js: GET/PATCH — manages user profiles.tts.js: Integrates OpenAI/Gemini TTS APIs._*.js: Internal helpers for KV, Supabase, Upstash, Groq, etc.
More detail in docs/ARCHITECTURE.md and CLAUDE.md.
- Groq free tier TPM (8000/min) is shared across the org. Re-measured on
openai/gpt-oss-120bwith real calls for the AI Response Contract V2 (grounding rules +votes[].condition+synthesis+protocol): ~1.75k–1.8k prompt + up to ~1.8k completion tokens ≈ 2.7k–3.6k total per debate depending on language and how much the model reasons — the same range as the pre-V2 prompt, so the whole site still sustains only ~2–3 debates/min before throttling. Re-measure before changingbuildPromptormaxTokens(see CLAUDE.md). Upstash Redis provides reliable production rate limiting; the Cloudflare KV fallback (used when Upstash isn't configured) is best-effort and can over-admit concurrent requests — seedocs/PRODUCTION_CHECKLIST.md. - AI Response Contract V2 adds grounding rules (no invented facts/statistics/private information — inferences must read as inferences), a
conditionon every "depends" vote, andsynthesis/protocolblocks (assumptions, unknowns, dissent, confidence, and a concrete next-48-hours/experiment/checkpoint/stop-condition).api/_validate.js'snormalizeDebate()accepts either shape and always flattens onto the same top-levelverdictstring, so V1 results already in Cloudflare KV (and their/r/:idshare links) keep rendering unchanged — they just don't show the synthesis/protocol panels, which are omitted (not shown empty) when absent. - Vote tallies are never simplified to a two-number score once "depends" is numerically significant.
councilHeadline()insrc/lib/share.jsnames all three counts and calls the result "divided" instead of collapsing e.g. a 4 yes / 3 depends / 2 no result into "leans yes, 4–2" — seesrc/lib/share.test.js. - If Groq is unreachable or fails for any reason other than rate limiting, the chamber shows an honest "could not reach the Council, try again" state — it never substitutes a fake debate for a real question (fixed after a real incident; see PR #75).
- Text-to-Speech (TTS) falls back to the browser's Web Speech API if both
OPENAI_API_KEYandGEMINI_TTS_API_KEYare missing or fail. - Deep Council is an optional, skippable extension of onboarding (3 short screens after the Quick Council path) capturing options considered, constraints, deadline, reversibility, cost of waiting, a picture of success, and what's known/unknown. All fields are optional and omitted entirely from the prompt when not provided — Quick Council behaves exactly as before.
- The reveal ceremony has a "Reveal all" control (visible once the first turn appears) that jumps straight to the verdict. Pacing also halves automatically for a returning visitor (any prior entry in
localStorage'scouncil:history), andprefers-reduced-motionnow collapses the stagedsetTimeoutdelays themselves, not just CSS transition durations — previously reduced-motion only shortened animations, not the ~40s wait those animations were staged behind. - Public sharing (WhatsApp/X/LinkedIn/Facebook/native share/copy link) requires an explicit preview step before anything is copied or opened — it shows exactly what the
/r/:idlink and share text/card expose, with an optional "redact personal details" toggle. Redaction only affects the share text/card being generated in that moment; it does not retroactively change the persisted/r/:idpage, which is disclosed in the same modal. "Copy as Text" and "Export JSON" remain private, unchanged local actions — they don't create or expose a public link.
See docs/CONTRIBUTING.md and CLAUDE.md.
MIT — see LICENSE.