Public
Live talk polling: QR join, ranked choice, demographic facets
pollingpresentationqr-codereactsqlitesurvey
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.

πŸ“Š Audience Survey

Live polling for talks. Show a QR code, let the room answer on their phones with no login, then reveal results on the projector β€” with ranked-choice runoffs, word clouds, and breakdowns by who's in the audience.

Create a survey Β· no account needed

How a talk goes

  1. Create β€” hit the homepage, name your survey. You get a secret admin link (bookmark it β€” it's the only way back).
  2. Build β€” add and edit questions as a private draft, then click Save questions to put new questions on deck. Mark ones like "What's your profession?" as Demographic so you can group everything else by them later.
  3. Share β€” put the fullscreen join slide (/s/<slug>/present) on the projector. It shows a big QR code + short URL.
  4. Release and collect β€” click Release to audience on each saved question when you reach it in the talk. Audience pages poll for newly released questions and present them one per screen. Each phone gets a cookie so answers are one-per-device and editable; no accounts.
  5. Reveal β€” flip Show results to audience. Open /s/<slug>/results on the projector; it live-updates every 3s. Use Group by to split every chart by a demographic question, or click a group chip to isolate it.
  6. Co-create β€” participants suggest complete questions and upvote others from the intro or thank-you screen. In the admin Proposals tab, review a draft, edit it, then approve it to append a live question. Finished participants see an invitation to answer newly added questions rather than being moved away from the community queue.

Community questions

Participants can propose any supported question type, including options, scale bounds, or matrix configuration. Pending proposals are public, sorted by upvotes (oldest first on ties), and refresh every 5 seconds while the page is visible. Each device can toggle one upvote per proposal; submitting a proposal does not automatically vote for it. Proposals do not require answering the survey first, and participants may upvote their own ideas.

The presenter sets required, demographic, and result-visibility flags during review. Approval is manual regardless of vote count, appends the edited question on deck, and removes its proposal from the pending queue. The presenter releases it separately when ready. Votes are not survey answers. There are no comments, participant edits, rejection actions, or automatic approvals.

Closing Accepting responses also closes proposals and voting, but leaves pending ideas readable. Admins may still approve while closed; participants can answer those questions when the survey reopens. Clearing responses keeps proposals and votes; deleting the survey removes them.

Proposal prompts are limited to 500 characters. Choice questions require 2–20 distinct nonempty options of up to 120 characters each. Scales use integer bounds between -100 and 100 and contain 2–21 steps. Matrix questions use a 2Γ—2 grid, a nonempty subject label of up to 120 characters, four nonempty endpoint labels of up to 60 characters, and up to 100 optional comparison-point labels of 120 characters each. Submission and approval requests are limited to 16 KB.

Question types

TypeRespondent seesResults visualization
Multiple choice (one)Tappable cardsColored bar chart; grouped bars when faceted
Multiple choice (many)Tappable cardsUpSet intersection plot with an option-total bar toggle; one plot per group when faceted
Scale / rating (e.g. 1–5, 0–10 NPS)Number gridHeat-colored histogram + animated mean
Ranked choiceTap-to-rank list with reorderInstant-runoff animation β€” step through elimination rounds, or switch to Borda points / first choices. Per-group winners when faceted
Word cloudSingle word/phraseSized, tilted word cloud
Free textTextareaSticky-note wall
Emoji reactionEmoji gridFloating emoji bubbles scaled by count
2Γ—2 matrixDrag/touch placement of a creator-labeled symbol anywhere in the plane, with tap and keyboard fallbacks, axis endpoints, and optional comparison pointsToggleable placement scatter plot and density heatmap with the same comparison points; one matrix per facet group

Admin controls

  • Save questions β€” persist question additions, edits, deletions, and reordering after reviewing the draft; newly saved questions remain on deck
  • Release to audience / Move on deck (per question) β€” immediately control whether a saved question can be seen and answered without publishing other unsaved edits; moving a question back on deck preserves answers already collected
  • Accepting responses β€” close the survey when you move on
  • Show results to audience β€” survey-wide reveal toggle
  • Hide results (per question) β€” keeps a question out of the audience view even when results are on
  • Audience can group results β€” let respondents use the facet controls too (off by default, keeps the projector clean)
  • Focus one question β€” projector-sized single-question view for pacing a talk
  • Proposals β€” review the audience's most-upvoted question drafts, edit, and approve
  • CSV export, clear responses, delete survey

Matrix questions have a creator-defined subject label for the symbol each respondent places. The respondent can drag it with a mouse, touch, or stylus, tap/click a destination, or use the arrow keys for precise movement. Answers are stored as normalized coordinates measured from the top-left: x increases from left to right and y increases from top to bottom. Merely viewing the centered unplaced symbol does not answer the question; the existing Required setting still controls whether it may be skipped.

Comparison points are optional read-only context, not additional answers. Each one appears in its cell as a symbol connected to a label, and question creators can drag the symbol and label independently to avoid visual overlap while keeping the full label inside its cell. Their relative placement is reused in the builder, respondent grid, scatter results, and density results. Each comparison point uses the same shape and color in the grid and retained legend; its legend entry includes a small highlighted grid showing its cell.

Results can switch between plotting every submitted placement and a 12Γ—12 density heatmap. Exact overlapping scatter points receive small deterministic offsets so repeated positions remain visible. CSV exports use stable values such as x 0.750, y 0.250 from top-left. Existing { row, column } answers remain compatible: they are interpreted at the center of the original quadrant and canonicalized to continuous coordinates when submitted again.

Architecture

Rendering mermaid diagram...

Security model: the admin key is a 28-char random token stored only as a SHA-256 hash. Respondents are identified by an HttpOnly cookie; clearing it or switching devices allows a second response or another proposal vote (acceptable trade-off for a login-free live poll β€” a "welcome back" banner nudges returning devices to edit instead). Public proposal payloads never expose device identifiers. Votes are unique per device and proposal, not per verified person; this is not strong abuse prevention.

Approval claims the pending proposal and appends its question on deck in a single SQLite write transaction. Regular builder edits stay local until Save questions is clicked. Release actions update only the persisted release flag, so they never publish unrelated draft edits. Saves compare their last persisted question snapshot to prevent stale tabs from overwriting an approval or release; conflicts retain local edits and offer a reload rather than silently losing questions.

Questions saved before progressive release was introduced are treated as released, preserving existing survey behavior. Public survey and results APIs omit on-deck questions, and crafted response requests cannot answer them. Admin results and CSV exports retain the full question set. If a released question is moved back on deck, its stored answers remain available if it is released again.

Files

index.ts                       Hono server: shell routes, public + admin API, QR SVG
backend/db.ts                  SQLite schema, hashing, random ids
backend/surveys.ts             CRUD, response upsert, answer sanitization
backend/aggregate.ts           per-type aggregation, pick-many intersections, instant-runoff, facet bucketing
backend/proposals.ts           proposal normalization, queries, approval workflow
backend/proposal-sql.ts        proposal/vote schema and atomic mutation statements
backend/errors.ts              expected API error types
shared/types.ts                Question/answer/result types shared by client & server
shared/questions.ts            shared question defaults and proposal validation
frontend/
  root.tsx                     HTML shell
  index.tsx                    React mount
  lib/api.ts                   typed fetch helpers
  lib/useProposals.ts          abortable live proposal polling
  components/App.tsx           path router
  pages/Home.tsx               create a survey
  pages/Admin.tsx              build Β· share Β· results tabs
  pages/Respond.tsx            one-question-per-screen flow
  lib/respondFlow.ts           active-question reconciliation across live releases
  pages/Results.tsx            public live results
  pages/Present.tsx            fullscreen QR join slide
  components/builder/          QuestionEditor and matrix configuration editor
  components/proposals/        participant composer, proposal cards, admin review
  components/respond/          QuestionInput, ranked drag/tap, and continuous matrix placement
  components/charts/           Charts, matrix scatter/density results, ResultsView (polling, facets)

Proposal regression tests

The tests use Deno's built-in runner and real in-memory SQLite through node:sqlite; the test configuration redirects only the Val Town SQLite import. No live survey data is used. The placeholder environment value lets the existing Val Town asset utility initialize without real credentials.

VAL_TOWN_API_KEY=local-test-placeholder deno test --allow-import --allow-env=VAL_TOWN_API_KEY \ --config tests/deno.json tests/api_test.ts shared/questions_test.ts backend/aggregate_test.ts backend/proposal-sql_test.ts \ frontend/lib/questionDraft_test.ts frontend/components/MatrixCellReferences_test.ts \ frontend/components/respond/MatrixInput_test.ts frontend/components/charts/MatrixHeatmap_test.ts