A real-time collaborative web application where users can toggle 1,000,000 checkboxes that stay in sync across all connected clients.
Million Checkboxes is a full-stack real-time application inspired by the "1 Million Checkboxes" concept. When any user toggles a checkbox, every other connected user sees the update instantly via WebSockets. The system is designed with scale in mind — using Redis bitfields for compact state storage, Redis Pub/Sub for multi-instance coordination, canvas-based rendering for performance, and custom rate limiting built from scratch.
| Layer | Technology |
|---|---|
| Frontend | HTML5 Canvas, Vanilla JS, CSS3 |
| Backend | Node.js, Express |
| Real-time | WebSockets (ws library) |
| State Storage | Redis (Bitfield) |
| Broadcasting | Redis Pub/Sub |
| Auth | OIDC / OAuth 2.0 (openid-client) + Demo login |
| Sessions | express-session |
- 1,000,000 checkbox grid rendered on
<canvas>for performance — no DOM nodes per checkbox - Real-time sync via WebSockets — toggles propagate to all connected clients in milliseconds
- Redis Bitfield storage — 1M bits stored in ~125KB (not 1M Redis keys!)
- Redis Pub/Sub — enables horizontal scaling across multiple server instances
- Custom rate limiting — no
express-rate-limitused; built with Redis INCR + sliding window - OIDC / OAuth 2.0 authentication — Google sign-in (or demo login for local testing)
- Virtual scrolling — only visible viewport is drawn on canvas
- Activity feed — live sidebar showing recent toggles
- Stats bar — connected users, checked/unchecked count, last toggled
- Jump to checkbox — navigate directly to any checkbox by number
- Graceful reconnect — WebSocket auto-reconnects on disconnect
- Anonymous read-only — viewers can watch without signing in; must sign in to toggle
- Node.js v18+
- Redis (local or Docker)
- A Google OAuth2 app (or use demo login — no config needed)
git clone https://github.com/yourname/million-checkboxes
cd million-checkboxes
npm installcp .env.example .env
# Edit .env — minimum required: SESSION_SECRET and REDIS_URL# Option A: Docker
docker run -d -p 6379:6379 redis:7-alpine
# Option B: Local Redis
redis-servernpm start
# or for development with auto-reload:
npm run devhttp://localhost:3000
Click Sign in → use demo login (no config needed) → start toggling!
| Variable | Required | Default | Description |
|---|---|---|---|
PORT |
No | 3000 |
HTTP server port |
SESSION_SECRET |
Yes | — | Secret for session signing (use a long random string) |
REDIS_URL |
No | redis://localhost:6379 |
Redis connection URL |
OIDC_ISSUER |
No | https://accounts.google.com |
OIDC provider issuer URL |
OIDC_CLIENT_ID |
No* | — | OAuth2 client ID from provider |
OIDC_CLIENT_SECRET |
No* | — | OAuth2 client secret |
OIDC_REDIRECT_URI |
No | http://localhost:3000/auth/callback |
OAuth2 callback URL |
APP_URL |
No | http://localhost:3000 |
Public app URL (for CORS) |
TOTAL_CHECKBOXES |
No | 1000000 |
Total number of checkboxes |
RATE_LIMIT_HTTP_WINDOW_MS |
No | 60000 |
HTTP rate limit window (ms) |
RATE_LIMIT_HTTP_MAX |
No | 100 |
Max HTTP requests per window |
RATE_LIMIT_WS_WINDOW_MS |
No | 1000 |
WebSocket rate limit window (ms) |
RATE_LIMIT_WS_MAX |
No | 5 |
Max WS toggle events per window |
*If not set, falls back to demo login automatically.
Redis serves three distinct roles in this application:
-
Bitfield storage — All 1,000,000 checkbox states stored as a single Redis bitfield key (
checkboxes:bits). Each checkbox = 1 bit. Total size ≈ 125KB. UsingBITFIELD/GETBIT/SETBITcommands. Far more efficient than 1M individual keys. -
Rate limiting — Sliding-window counters stored as
ratelimit:<id>:<window_bucket>keys with TTL-based expiry.INCR + PEXPIREin a pipeline = atomic enough for our needs. -
Pub/Sub broadcasting — When a toggle happens, we
PUBLISHtocheckboxes:updates. Every server instance has a subscriber that re-broadcasts to its local WebSocket clients. This enables horizontal scaling.
checkboxes:bits — Bitfield: all 1M checkbox states
ratelimit:<id>:<bucket> — Rate limit counters (TTL = 2 windows)
connected:sockets — Hash: socketId → userId (for counting)
stats:total_connections — Counter: all-time connection count
# Docker (recommended)
docker run -d --name checkboxes-redis -p 6379:6379 redis:7-alpine
# Verify
redis-cli ping # → PONG
# Inspect state
redis-cli getbit checkboxes:bits 0 # is checkbox #1 checked?
redis-cli bitcount checkboxes:bits # how many are checked?User → /auth/login
│
├─ OIDC configured? ──YES──→ Redirect to Google (with nonce + state)
│ │
│ Google authenticates user
│ │
│ Redirect to /auth/callback?code=...
│ │
│ Server: exchange code → tokens → userinfo
│ │
└─ No (demo mode) ─→ /auth/demo-login → POST /auth/demo-callback
│
Session: { sub, name, email }
│
Redirect to /
- Session is stored server-side via
express-session - Session cookie is
httpOnly(XSS protection) andsecurein production - Session is attached to WebSocket upgrade requests by piping through session middleware
- WebSocket handler reads
req.session.userto determine authenticated identity - Anonymous users get read-only access; toggles are rejected with
AUTH_REQUIREDerror
Client connects to ws://host/ws
│
Server reads session from HTTP upgrade headers
│
Server sends { type: 'init', data: <base64 bitfield>, checkedCount, connectedCount }
│
Client decodes bitfield → renders full canvas grid
│
─────────── User toggles checkbox ──────────
│
Client sends { type: 'toggle', index: 42345 }
│
Server: rate limit check → auth check → Redis SETBIT → Redis PUBLISH
│
Redis Pub/Sub → received by ALL server instances
│
Each server instance → broadcast { type: 'update', index, value } to local WS clients
│
Clients update their local bit array + redraw the changed checkbox
Events:
| Direction | Type | Payload |
|---|---|---|
| C→S | toggle |
{ index } |
| C→S | ping |
— |
| S→C | init |
{ total, data, checkedCount, connectedCount, socketId } |
| S→C | update |
{ index, value, userId, userName } |
| S→C | stats |
{ checkedCount, connectedCount } |
| S→C | ratelimit |
{ retryAfter } |
| S→C | error |
{ message, code } |
| S→C | pong |
— |
Rate limiting is implemented entirely from scratch using Redis — no express-rate-limit or similar packages.
identifier = userId OR IP address OR socketId
window_bucket = floor(now_ms / window_size_ms)
key = "ratelimit:<identifier>:<window_bucket>"
INCR key → count
PEXPIRE key (window_size * 2) ← ensures cleanup even if we miss it
if count > max_requests → reject with 429 / ratelimit event
- Window: 60 seconds
- Max: 100 requests per window per user/IP
- Identifies by:
userId(if authenticated) elseIP address - Returns
X-RateLimit-*headers on every response - Returns
429 Too Many Requestswhen exceeded
- Window: 1 second
- Max: 5 toggle events per window per user/socketId
- Two-tier: fast local in-memory pre-check + Redis-backed accurate check
- Local cache avoids Redis round-trip for obvious spammers (3× limit triggers local block)
- Returns
{ type: 'ratelimit', retryAfter }message to client
A malicious client could spam thousands of messages per second. The local cache blocks them immediately without hitting Redis. The Redis check ensures fairness across multiple server instances.
million-checkboxes/
├── src/
│ ├── server.js # Express + HTTP + WebSocket server
│ ├── config/
│ │ └── oidc.js # OIDC client init + auth middleware
│ ├── routes/
│ │ ├── auth.js # /auth/* routes (login, callback, logout)
│ │ └── api.js # /api/* routes (state, stats)
│ ├── services/
│ │ ├── redis.js # All Redis operations
│ │ └── websocket.js # WS connection handler + Pub/Sub
│ └── middleware/
│ └── rateLimiter.js # Custom rate limiting (HTTP + WS)
├── public/
│ ├── index.html # SPA shell
│ ├── css/style.css # All styles
│ └── js/app.js # Canvas renderer + WS client
├── .env.example # Environment variable template
├── package.json
└── README.md
Using a Redis Bitfield — each checkbox = 1 bit. SETBIT checkboxes:bits <index> <0|1>. Total: 1,000,000 bits = 125,000 bytes ≈ 125KB. Compared to 1M individual keys, this is ~1000x more memory-efficient.
Using an HTML5 <canvas> element as the rendering target. No DOM nodes are created per checkbox. Only the visible viewport is drawn. On scroll, only the newly visible area is drawn. This handles 1M checkboxes at 60fps.
Redis SETBIT is atomic. One write wins. The final state is consistent because both clients will receive the update message broadcast via Pub/Sub and sync to the actual Redis state.
Every server instance subscribes to checkboxes:updates. When server A processes a toggle, it publishes to that channel. Server B (and server A itself) receive the message via their subscriber and broadcast to their local WebSocket clients. No direct server-to-server communication needed.
MIT