A real-time collaborative whiteboard where anyone can sketch, write, drop images, and leave comments on an infinite canvas — together, live, with zero friction. Start a party in one click and share a code, or sign up to keep your boards forever.
Most whiteboards make you sign up before you can draw a single line. Flux flips that. Click Start a party, get a shareable code like FLX-9K4T, and your teammates are drawing on the same canvas seconds later — no accounts, no installs, nothing to configure. When you want permanence, sign up and your boards live in a dashboard forever (you can even claim a party board into your account).
Under the hood it's not a toy. Concurrent edits are merged with a conflict-free replicated data type (CRDT), so two people erasing and drawing over each other never corrupt the board — every client converges to the same picture, even after a disconnect and replay. State is persisted to Postgres as an operation log, periodically compacted into snapshots in object storage, and fanned out across server instances through Redis pub/sub.
| ⚡ Party mode | ✦ Account mode | |
|---|---|---|
| Sign up? | Never | Free |
| Get going | One click → share a FLX-XXXX code |
Email + password |
| Boards | Expire after 24h of inactivity | Saved forever |
| Dashboard | — | Manage all your boards |
| Claim | — | Pull a party board into your account |
| Export | PNG / PDF | PNG / PDF |
🎨 A real drawing surface
- Infinite, pannable, zoomable canvas with a live minimap for navigation
- Pen, highlighter (semi-transparent), and eraser tools
- Text with full formatting — font family, size, bold / italic / underline, multi-line
- Images — drag-and-drop or paste straight onto the canvas (uploaded via presigned URLs with an on-canvas progress ring)
- Selection model — marquee + lasso select, move, 8-handle resize (images too), delete, double-click to re-edit text
👥 Truly collaborative
- Live multi-user editing with presence and real-time cursors
- CRDT merge (LWW-Element-Set + vector clocks) so concurrent edits converge — no lost strokes, no corruption
- Comments anchored to individual shapes, with threads and unread indicators
- Resilient reconnect — drop your wifi, come back, and the board replays to a consistent state
🗂 Persistence & sharing
- Operation log persisted to Postgres; periodic snapshots to S3-compatible object storage with compaction
- Export any board to PNG or PDF, entirely client-side
- Per-board light / dark theme, persisted
| Layer | Technology |
|---|---|
| Frontend | React 18 · TypeScript · Vite · Zustand · React Router · custom canvas engine |
| Realtime | WebSocket over STOMP + SockJS |
| Backend | Spring Boot 3.3 · Java 21 · Spring Security (JWT) · Spring Batch |
| Merge | Custom CRDT — LWW-Element-Set + vector clock |
| Database | PostgreSQL (Neon) · Flyway migrations |
| Cache / fan-out | Redis (Upstash) pub/sub — optional for single-instance |
| Object storage | AWS S3 / Cloudflare R2 (presigned uploads) |
| Export | jsPDF |
| Deploy | Vercel (frontend) · Render (backend, Docker) |
┌──────────────────────────────────────────────┐
│ Browser (React) │
│ Canvas engine · CRDT client · STOMP client │
└───────────────┬──────────────┬───────────────┘
REST/JWT │ │ WebSocket (STOMP)
▼ ▼
┌──────────────────────────────────────────────┐
│ Spring Boot backend │
│ Auth · Boards · Party · Draw/Presence WS │
│ CRDT merge service · Snapshot & batch jobs │
└───┬───────────────┬───────────────┬──────────┘
│ │ │
┌───────▼──────┐ ┌──────▼──────┐ ┌──────▼───────┐
│ PostgreSQL │ │ Redis │ │ S3 / R2 │
│ op log + │ │ cross- │ │ snapshots + │
│ snapshots │ │ instance │ │ image │
│ (Neon) │ │ fan-out │ │ uploads │
└──────────────┘ └─────────────┘ └──────────────┘
How a stroke travels: you draw → the op is applied locally and merged into the client CRDT → published over STOMP → the backend merges it into the authoritative CRDT, persists it to the op log, and re-broadcasts. With multiple instances, Redis pub/sub fans the op out to clients connected to other nodes. Every so often a batch job compacts the op log into a snapshot in object storage so new joiners load fast.
flux/
├── backend/ Spring Boot 3 · Java 21
│ └── src/main/java/com/flux/
│ ├── controller/ REST: Auth, Board, Party, Comment, Health
│ ├── handler/ WebSocket: DrawOperation, Presence, Redis subscriber
│ ├── service/ CRDT, Board, Party, Auth, Jwt, Snapshot, Image, Presence
│ ├── crdt/ LwwElementSet, VectorClock
│ ├── batch/ Snapshot, PartyCleanup, KeepAlive jobs
│ ├── model/ JPA entities
│ └── config/ Security, WebSocket, Redis, R2/S3, CORS
│
└── frontend/ React 18 · Vite · TypeScript
└── src/
├── canvas/ CanvasEngine, ViewportManager
├── crdt/ ClientCrdt
├── ws/ StompClient
├── pages/ Landing, Party, Board, Dashboard, Auth
└── components/ Minimap, HistoryPanel, ExportModal, CommentThread, BoardCard
- Java 21
- Node.js 20+
- Free accounts for the cloud services below (or point them at local equivalents)
Flux talks to managed services. Create the following and collect their credentials:
- Neon — Postgres connection string
- Upstash — Redis URL (optional; set
REDIS_ENABLED=falseto skip on a single instance) - AWS S3 or Cloudflare R2 — a bucket for snapshots & image uploads
Export them into your shell:
export NEON_DATABASE_URL="jdbc:postgresql://ep-xxxx.region.aws.neon.tech/fluxdb?sslmode=require"
export UPSTASH_REDIS_URL="redis://default:password@your-host.upstash.io:port"
export JWT_SECRET="your-256-bit-random-string-here"
export S3_BUCKET="flux-whiteboard-snapshots"
export R2_ACCESS_KEY_ID="your-access-key"
export R2_SECRET_ACCESS_KEY="your-secret-key"
export AWS_REGION="auto" # a real AWS region for S3; "auto" for R2
# export R2_ENDPOINT="https://<account-id>.r2.cloudflarestorage.com" # only for R2cd backend
./mvnw spring-boot:run # runs Flyway migrations on startup, then serves on :8080cd frontend
npm install
npm run dev # http://localhost:5173Open http://localhost:5173 and start a party.
Java version error?
class file version 61.0 ... up to 52.0means Maven picked up an older JDK. Make sureJAVA_HOMEpoints at your Java 21 install before running./mvnw.
The live app runs on a split deployment:
- Frontend → Vercel. Build
frontend/; setVITE_API_URLto the backend origin. - Backend → Render (Docker, see
render.yaml). Set all secrets in the dashboard, plusALLOWED_ORIGINSto include your deployed frontend URL so WebSocket/CORS handshakes succeed.
The backend honours an injected $PORT, exposes /healthz for health checks, and runs a keep-alive job to avoid free-tier spin-down.
- Party mode + account mode with board claiming
- CRDT merge, persistence, snapshots & compaction
- Presence, live cursors, reconnect resilience
- Full tool set — pen, highlighter, eraser, styled text, images
- Selection model — marquee/lasso, move, resize, re-edit
- Shape comments with threads
- PNG / PDF export
- Server-side ownership hardening on comment edit/delete
- World-coordinate remote cursors across zoom levels
- Keep comments anchored through shape move/resize
Built with Spring Boot, React, and a hand-rolled CRDT.