Skip to content

dnano-more/million-checkboxes

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

⬛ Million Checkboxes

A real-time collaborative web application where users can toggle 1,000,000 checkboxes that stay in sync across all connected clients.

Million Checkboxes Screenshot


📋 Project Overview

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.


🛠 Tech Stack

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

✅ Features Implemented

  • 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-limit used; 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

🚀 How to Run Locally

Prerequisites

  • Node.js v18+
  • Redis (local or Docker)
  • A Google OAuth2 app (or use demo login — no config needed)

1. Clone and install

git clone https://github.com/yourname/million-checkboxes
cd million-checkboxes
npm install

2. Configure environment

cp .env.example .env
# Edit .env — minimum required: SESSION_SECRET and REDIS_URL

3. Start Redis

# Option A: Docker
docker run -d -p 6379:6379 redis:7-alpine

# Option B: Local Redis
redis-server

4. Start the server

npm start
# or for development with auto-reload:
npm run dev

5. Open in browser

http://localhost:3000

Click Sign in → use demo login (no config needed) → start toggling!


🔐 Environment Variables

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 Setup

Why Redis?

Redis serves three distinct roles in this application:

  1. Bitfield storage — All 1,000,000 checkbox states stored as a single Redis bitfield key (checkboxes:bits). Each checkbox = 1 bit. Total size ≈ 125KB. Using BITFIELD / GETBIT / SETBIT commands. Far more efficient than 1M individual keys.

  2. Rate limiting — Sliding-window counters stored as ratelimit:<id>:<window_bucket> keys with TTL-based expiry. INCR + PEXPIRE in a pipeline = atomic enough for our needs.

  3. Pub/Sub broadcasting — When a toggle happens, we PUBLISH to checkboxes:updates. Every server instance has a subscriber that re-broadcasts to its local WebSocket clients. This enables horizontal scaling.

Redis Key Naming

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

Local Redis Quick-Start

# 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?

🔒 Auth Flow Explanation

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) and secure in production
  • Session is attached to WebSocket upgrade requests by piping through session middleware
  • WebSocket handler reads req.session.user to determine authenticated identity
  • Anonymous users get read-only access; toggles are rejected with AUTH_REQUIRED error

🔌 WebSocket Flow Explanation

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 Logic

Rate limiting is implemented entirely from scratch using Redis — no express-rate-limit or similar packages.

Algorithm: Fixed Window Counter

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

HTTP Rate Limiting (src/middleware/rateLimiter.js)

  • Window: 60 seconds
  • Max: 100 requests per window per user/IP
  • Identifies by: userId (if authenticated) else IP address
  • Returns X-RateLimit-* headers on every response
  • Returns 429 Too Many Requests when exceeded

WebSocket Rate Limiting

  • 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

Why two tiers for WebSocket?

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.


🏗 Project Structure

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

🎯 System Design Decisions

How are 1M checkbox states stored?

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.

How does the frontend render without crashing?

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.

What if two users toggle the same checkbox simultaneously?

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.

How does Pub/Sub enable multi-server scaling?

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.


📄 License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors