Surprisal Live Kit
Live polls, word clouds, quizzes and Q&A for any room.
People join on their phone with a 4-letter code. No app, no account.
An open-source, self-hosted alternative to Mentimeter, Slido and Kahoot.
Quick start · Deploy · Make it yours · Roadmap · Contributing
Share the presenter screen in a call or put it on a projector. People scan the QR code, type their name and answer on their phone. The screen counts the answers as they arrive, and the results stay hidden until you reveal them.
It is one repository that you deploy to the free plans of Vercel and Supabase. Your name and logo go on the screen, and the answers stay in your own database.
Nine people join, a phone votes, the host reveals the results and the room reacts. Recorded with npm run screenshots.
Features
- 🗳️ 12 slide types. Polls (bars, columns, donut, multi-select), word clouds, open text, scales (1 to 5, 1 to 10, agree / disagree), ranking, 100 points, prediction, 2×2, timed quizzes with points and a podium, leaderboards, and Q&A with upvotes.
- 🙈 Results wait for you. The screen counts the answers as they arrive. Open answers show as face-down cards. Press Reveal results or the R key when the room is ready.
- 👏 Reactions. 👏 🤯 😂 ❤️ 🤔 rise on the screen, and the phone confirms each one.
- 🚪 Doors open. The first slide shows the QR code and the join code. Each name that joins falls onto a pile of cards.
- ⚖️ Before and after. Ask a poll or a scale again after the talk, and the screen shows how the room moved.
- 🎛️ A console for the host. Notes, the next slide, held answers and every control, in a second window or on your phone. The room does not see it.
- 🧹 A word filter. Answers with swear words or slurs (English, Swedish, Portuguese) wait in the console until you approve them.
- 📊 A report after each session. A CSV and JSON export, and a Markdown write-up to paste into a follow-up email.
- 🏷️ White-label. Your name, logo, colours, fonts and join address.
- 🔒 Nothing to sign up for. A person is a random id on the phone plus the name they type. The app collects no email addresses.
- 🌗 Light and dark, keyboard shortcuts, self-paced decks for surveys, and sound for the quiz (off by default).
Every slide type has its own reveal
![]() Poll · bars, columns or a donut |
![]() Word cloud · up to three words each |
![]() Quiz · timed, scored, with a podium |
![]() 2×2 · every answer a dot |
![]() Agree / disagree · now against before |
![]() Ranking · the room's order |
![]() Open text · spotlight one, hide one |
![]() Q&A · upvotes, mark as answered |
![]() 100 points · where the room puts its weight |
On the phone
Quick start
You need Node.js 20.19 or later. You do not need any accounts.
git clone https://github.com/habit-ai/surprisal-live-kit.git
cd surprisal-live-kit
npm install
npm run dev
- Open the Network address that the terminal shows, plus
/live/host. For example:http://192.168.1.20:5173/live/host. - Click a template, then click Go live.
- Scan the QR code with a phone on the same Wi-Fi network.
Use the Network address, not localhost. The QR code points to the address in your browser, and a phone cannot open
localhost on your computer.
To see every slide type with answers, run npm run demo in a second terminal. It makes a room with 24 slides,
fills it with 32 sample people and prints the presenter link. If the dev server is not on port 5173, set LIVE_URL.
The local server keeps everything in memory. A restart deletes all sessions. Anyone who can open the page can go live. For a real session with people in other places, deploy it.
Deploy
Vercel and Supabase both have free plans.
1. Make the database
- Make a project at supabase.com.
- Open SQL Editor. Paste all of
supabase/migrations/0001_live.sql. Click Run. - Open Project Settings > API. Copy the Project URL and the service_role key.
The seven tables have row level security on and no policies. Only the server function can read or write them, with
the service key. Do not put the service key in the browser or in a VITE_ variable.
2. Deploy the app
The button copies this repository to your GitHub account and asks for three environment variables. To do it by hand, import the repository at vercel.com/new and add the same variables before the first deploy.
| Name | Value |
|---|---|
SUPABASE_URL |
The Project URL from step 1 |
SUPABASE_SERVICE_ROLE_KEY |
The service_role key from step 1 |
LIVE_ADMIN_KEY |
A long random string. Make one with openssl rand -hex 24. |
Then open https://your-app.vercel.app/live/host, click Connect shared library and type your LIVE_ADMIN_KEY.
Each host types the admin key once per browser. The key lets them go live and share decks in the library. Give it only to people who host. Phones never need it.
3. Put the server near the database
vercel.json runs the function in London (lhr1), next to a Supabase project in eu-west-2. If your Supabase project
is in another region, change regions to the nearest Vercel region.
Every answer is a round trip from the function to the database, so this changes how fast the screen updates.
Make it yours
Brand settings are environment variables. Vite reads them at build time, so redeploy after a change. For a local
build, copy .env.example to .env.
| Name | What it does | Default |
|---|---|---|
VITE_BRAND_NAME |
Your name in the top bars, the browser tab and the report | Live |
VITE_BRAND_LOGO |
A file in public/ or a full URL. Set it to an empty value to show the name only. |
/logo.svg |
VITE_BRAND_URL |
Where the phone's last screen links to | no link |
VITE_JOIN_HOST |
The address the screen shows for phones, for example live.example.org |
the site's own address |
Colours and fonts are in src/styles/theme.css. The comment at the top of that file lists
the tokens to change first. Change the dark values too, and keep text at a contrast of 4.5:1 or more.
- A one-colour logo turns white in dark mode. For a colour logo, set
--brand-logo-dark-filter: noneintheme.css. - Replace
public/favicon.svgwith your own icon. - The templates on the host page are in
TEMPLATESinsrc/lib/live.ts. - The word filter's list is
FLAGGEDin the same file. npm run screenshotsmakes the images in this README again, in your colours. The GIF needsffmpeg.
Run a session
- Make a deck on the host page, or start from a template. Click Go live.
- Share the presenter window in your call, or put it on the projector. Keep the window uncovered: a hidden tab stops updating.
- Press C to open the console in a second window. To run the session from your phone, scan the console's QR code. That link gives full host control, so do not post it in a chat.
- Move between slides with the arrow keys. Press R to reveal or hide the results.
- After the session, press End, then open the Report. The report also opens later from the host page.
Keyboard shortcuts
| Key | Action |
|---|---|
| → / ← (or Page Down / Page Up, Space) | Next / previous slide |
| R | Reveal or hide the results |
| V | Open or close voting |
| T | Restart the slide's timer |
| J | Show the join code full screen |
| C | Open the console |
| F | Full screen |
| S | Sound on or off |
| Esc | Close an overlay |
The host key for each session is kept in the browser that made it. To present from another browser, open the console's link (its QR code) there.
How it works
flowchart LR
phone["📱 Phones<br/>/live"] -- "join, answer, react<br/>(poll every 1.2 s)" --> fn
screen["🖥️ Presenter and console<br/>/live/host"] -- "control<br/>(poll every 0.9 s)" --> fn
fn["Vercel function<br/>api/live.ts"] -- "PostgREST with<br/>the service key" --> db[("Supabase Postgres<br/>7 tables")]
- No websockets. Phones and the presenter poll one function. There is no connection to keep open, and it runs on the free plans.
- One handler. All the server logic is in
api/_lib/live.ts. It runs on an in-memory store fornpm run devand the tests, and on Supabase in production. - React on the client. Two pages:
live.htmlfor phones andlive-host.htmlfor the host library, editor, presenter, console and report.
Tested with a full room
On surprisal.to, this code ran a load test with 45 phones at a normal pace and 150 phones that all answered within 2 seconds. No answers were lost. The 95th percentile was 288 ms for an answer and 150 ms for a room poll.
npm test # 32 tests on the server logic, in memory
npx playwright install chromium # once
npm run smoke # one whole session in a real browser: host, phone, poll, reveal, reaction, end
npm run build # type check and production build
GitHub Actions runs all three on every push and pull request.
Roadmap and known issues
Help is welcome on any of these. See CONTRIBUTING.md.
- Two hand-over links opened at the same moment in one browser can drop one session from the host page's list. Open them one at a time for now.
- A session opened from a hand-over link shows as "Live session" on the host page, not with its deck title.
- At window sizes of 1440×790 or less, a 6-option poll and the prediction chart can run under the bottom bar.
- The rate limits are kept per server instance, so they slow abuse but are not a hard wall.
- Names under reactions (the server does not send them yet).
- Push instead of polling, for rooms of many hundreds.
- The word filter in more languages.
Contributing
Bug reports, fixes and new slide types are welcome. CONTRIBUTING.md shows how to run it, where the code is and which checks to run before a pull request. To report a security problem, read SECURITY.md.
License
MIT. See LICENSE.
Surprisal made it for its member sessions and webinars, and this kit is the same code without the Surprisal brand. If you run it for your community, we would like to hear about it. If it is useful to you, a ⭐ helps other people find it.









