Civic Discourse — Weekly Vote

Do you agree with the 1st Amendment to the Constitution?

Cast your YES / NO vote. Results update live. AI summary publishes at close.

Current Topic Closes (HAPPY HOUR): loading… (Fri 5 PM ET) Total votes: 0
YES50%
NO50%

After you vote, your choice is locked for this topic. New topics open each Friday at 5 PM ET.

Time Remaining
00:00:00
Ends —

AI Summary (Preview)

Debate sentiment and key arguments will appear here at close.

© Civic Discourse. All rights reserved. Built for transparency and constructive dialogue.
/** * Cloudflare Worker (Modules syntax) * Endpoints: * GET /tally?topic=topic-key -> { yes, no } * POST /vote -> { ok, yes, no, accepted } * * D1 tables: * tallies(topic_key TEXT PK, yes INT, no INT) * votes(topic_key TEXT, email_hash TEXT, choice TEXT CHECK('YES'/'NO'), ts TEXT, PRIMARY KEY(topic_key,email_hash)) * * CORS: set CORS_ORIGIN in wrangler.toml for stricter origin; defaults to "*". */ export default { async fetch(req, env) { const url = new URL(req.url); const origin = env.CORS_ORIGIN || "*"; const cors = { "Access-Control-Allow-Origin": origin, "Access-Control-Allow-Headers": "content-type", "Access-Control-Allow-Methods": "GET,POST,OPTIONS", }; if (req.method === "OPTIONS") { return new Response(null, { headers: cors }); } // Health if (url.pathname === "/health") { return new Response(JSON.stringify({ ok: true }), { headers: { "content-type": "application/json", ...cors }}); } // GET /tally?topic=... if (req.method === "GET" && url.pathname === "/tally") { const topic = url.searchParams.get("topic") || ""; if (!topic) { return new Response(JSON.stringify({ error: "missing topic" }), { status: 400, headers: { "content-type": "application/json", ...cors } }); } const row = await env.DB.prepare( "SELECT yes, no FROM tallies WHERE topic_key = ?1" ).bind(topic).first(); const yes = row?.yes ?? 0; const no = row?.no ?? 0; return new Response(JSON.stringify({ yes, no }), { headers: { "content-type": "application/json", ...cors } }); } // POST /vote if (req.method === "POST" && url.pathname === "/vote") { let payload; try { payload = await req.json(); } catch { return new Response(JSON.stringify({ error: "bad json" }), { status: 400, headers: { "content-type": "application/json", ...cors } }); } const topic = String(payload.vote_topic_key || "").trim(); const choice = String(payload.vote_choice || "").trim().toUpperCase(); const email = String(payload.email || "").trim(); const name = String(payload.name || "").trim(); const phone = String(payload.phone || "").trim(); const ts = String(payload.vote_timestamp || new Date().toISOString()); if (!topic || !email || (choice !== "YES" && choice !== "NO")) { return new Response(JSON.stringify({ error: "missing fields" }), { status: 400, headers: { "content-type": "application/json", ...cors } }); } // Hash email (lowercased) const emailHash = await sha256Lower(email); // Ensure tallies row exists await env.DB.prepare( "INSERT OR IGNORE INTO tallies (topic_key, yes, no) VALUES (?1, 0, 0)" ).bind(topic).run(); // One vote per (topic, email_hash). If first time, insert and increment tally. const insert = await env.DB.prepare( "INSERT OR IGNORE INTO votes (topic_key, email_hash, choice, ts) VALUES (?1, ?2, ?3, ?4)" ).bind(topic, emailHash, choice, ts).run(); if (insert.meta.changes === 1) { const col = choice === "YES" ? "yes" : "no"; await env.DB.prepare(`UPDATE tallies SET ${col} = ${col} + 1 WHERE topic_key = ?1`).bind(topic).run(); } // Current tallies const row = await env.DB.prepare( "SELECT yes, no FROM tallies WHERE topic_key = ?1" ).bind(topic).first(); return new Response(JSON.stringify({ ok: true, yes: row?.yes ?? 0, no: row?.no ?? 0, accepted: insert.meta.changes === 1 }), { headers: { "content-type": "application/json", ...cors }}); } return new Response("Not found", { status: 404, headers: cors }); } }; /** Hash helpers */ async function sha256Lower(email) { const data = new TextEncoder().encode(email.toLowerCase()); const hash = await crypto.subtle.digest("SHA-256", data); return [...new Uint8Array(hash)].map(b => b.toString(16).padStart(2, "0")).join(""); } -- setup.sql (run once to init the database) CREATE TABLE IF NOT EXISTS tallies ( topic_key TEXT PRIMARY KEY, yes INTEGER NOT NULL DEFAULT 0, no INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS votes ( topic_key TEXT NOT NULL, email_hash TEXT NOT NULL, choice TEXT NOT NULL CHECK (choice IN ('YES','NO')), ts TEXT NOT NULL DEFAULT (datetime('now')), PRIMARY KEY (topic_key, email_hash) );
Civic Discourse — Neon Vote

Do you agree with the 1st Amendment to the Constitution?

Cast your YES / NO vote. Results update live. AI summary publishes at close.

Current Topic Closes: loading… Total votes: 0
YES68%
NO32%

After you vote, your choice is locked for this topic. New topics open every 7 days.

Time Remaining
02:16
Ends —

AI Summary (Preview)

Debate sentiment and key arguments will appear here at close.

© Civic Discourse. All rights reserved. Built for transparency and constructive dialogue.
Civic-Discourse.com — Landing Mockup v34
CDDDCCDDDCCDDDCCDDDCCDDDC CDDDCCDDDCCDDDCCDDDCCDDDC CDDDCCDDDCCDDDCCDDDCCDDDC CDDDCCDDDCCDDDCCDDDCCDDDC
Civic Dash Discourse Dot ComCivic Dash Discourse Dot ComCivic Dash Discourse Dot ComCivic Dash Discourse Dot Com Civic Dash Discourse Dot ComCivic Dash Discourse Dot ComCivic Dash Discourse Dot ComCivic Dash Discourse Dot Com Civic Dash Discourse Dot ComCivic Dash Discourse Dot ComCivic Dash Discourse Dot ComCivic Dash Discourse Dot Com
Countdown:
6 DAYS
23 HOURS
59 MINUTES
12 SECONDS
Cast Your Vote before it is too late
and make your voice be heard
YES:  |  NO:
Voting ends in
Loading…