Anonymous polls that just work.
QikPoll is a StrawPoll-style app for fast, no-signup polling. Anyone can create a poll, share a link, and watch results update live in real time.
- Create polls in seconds with 2 to 8 options.
- Use
publicvisibility to list polls in the home feed. - Use
privatevisibility for unlisted direct-link access. - Anonymous voting with anti-repeat guardrails.
- Live vote updates on poll pages via WebSockets.
- Live updates for the recent public poll feed.
- Redis-backed storage with TTL expiration.
| Area | Technology |
|---|---|
| Runtime | Bun |
| Language | TypeScript |
| Frontend | React 19 |
| App framework | TanStack Start + TanStack Router |
| Styling/UI | CSS + Lucide icons |
| Data store | Redis 5 |
| Realtime | WebSockets + Redis Pub/Sub |
| Build tooling | Vite 7 |
| Quality tooling | Biome + Vitest |
src/routes/index.tsx: Home screen, poll creation, and recent public polls feed.src/routes/p/$pollId.tsx: Poll voting page with live results.src/routes/api/polls.tsx: Poll create/fetch/list handlers.src/routes/api/polls.vote.tsx: Vote submission endpoint.src/routes/api/live.tsx: WebSocket upgrade endpoint.src/lib/server/polls.ts: Poll domain logic, vote restrictions, rate limiting.src/lib/server/poll-live.ts: Pub/Sub fanout for realtime updates.src/lib/server/redis.ts: Shared Redis client.
poll:<id>stores the poll JSON document.poll:index:publicstores recent public poll IDs in a sorted set.poll:vote:fp:<pollId>:<fingerprintHash>enforces one vote per fingerprint.poll:vote:ip:<pollId>:<ipHash>adds one-vote guardrail by hashed IP.poll:rate:create:<ipHash>limits poll creation bursts.poll:rate:vote:<pollId>:<ipHash>limits vote-attempt bursts.
GET /api/live?pollId=<pollId>streams per-poll vote updates.GET /api/live?stream=publicstreams recent public poll feed updates.
- Install dependencies.
bun install- Start Redis locally.
redis-server- Create
.env.
REDIS_URL=redis://localhost:6379
POLL_TTL_SECONDS=604800
POLL_FINGERPRINT_SALT=change-me-in-production- Start the app.
bun --bun run devApp URL: http://localhost:3000
POST /api/pollscreates a poll.GET /api/polls?id=<pollId>fetches a poll by ID.GET /api/polls?limit=<n>lists recent public polls.POST /api/polls/votesubmits a vote.POST /api/mcpMCP JSON-RPC endpoint for remote MCP clients.GET /api/live?pollId=<pollId>upgrades to poll websocket stream.GET /api/live?stream=publicupgrades to public feed websocket stream.
This repo includes an MCP endpoint served by the same TanStack Start server:
/api/mcp.
It calls the existing poll logic, so MCP users do not need direct Redis access.
Tools exposed:
create_pollcreate a poll (title,options, optionalvisibility).list_public_pollslist public polls (limitoptional).get_pollfetch poll details and current results bypollId.vote_pollsubmit vote (pollId,optionId).
Example OpenCode remote MCP config (~/.config/opencode/opencode.json):
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"qikpoll": {
"type": "remote",
"url": "https://your-qikpoll-app.example.com/api/mcp",
"enabled": true
}
}
}Use your deployed app URL for url.
This OpenCode setup uses a remote MCP endpoint hosted by your app server.
REDIS_URLrequired Redis connection URL.POLL_TTL_SECONDSpoll and vote lock expiration in seconds.POLL_FINGERPRINT_SALTsalt used in hashing anti-abuse identifiers.
bun --bun run devstart dev server.bun --bun run buildbuild for production.bun --bun run previewrun preview server.bun --bun run mcpoptional standalone stdio MCP server for local testing.bun --bun run testrun tests.bun --bun run lintrun lint checks.bun --bun run checkrun full Biome checks.bun --bun run formatformat code.
- Raw IP addresses are not stored; only salted hashes are used.
- Vote writes are atomic using a Redis Lua script to reduce race-condition double-votes.
- Poll and vote lock keys share TTL expiration behavior.
