A real-time collaborative note-taking application built with React, Node.js, Supabase, and Yjs.
- Document management with soft delete and restore from trash
- Rich text editing with Tiptap — headings, bullet lists, code blocks, slash commands (
/heading,/code, etc.) - Real-time collaboration via Yjs CRDTs + Hocuspocus WebSocket server
- Presence indicators showing live collaborators with colored avatars
- Debounced autosave — content persists without excessive writes
- Document version history with restore to any previous version
- User-specific workspaces — isolated per user via Supabase Auth
| Layer | Technology |
|---|---|
| Frontend | React 18 + TypeScript + Vite |
| Styling | TailwindCSS |
| Editor | Tiptap + ProseMirror |
| Real-time Sync | Yjs (CRDT) + Hocuspocus WebSocket |
| Backend | Node.js + TypeScript + Express |
| Database/Auth | Supabase (PostgreSQL + GoTrue) |
| Containerization | Docker + Docker Compose |
The application is structured in three layers:
-
Frontend SPA (React + Vite, port 5173) — communicates with the Express REST API for document CRUD operations. The Tiptap editor uses
HocuspocusProviderto connect to the Hocuspocus WebSocket server for real-time Yjs sync. -
Backend (Node.js + Express, port 3001 + Hocuspocus port 1234) — Express handles REST API routes (auth, documents, versions). Hocuspocus runs as a separate HTTP server in the same process, handling WebSocket connections, document load/store, and awareness broadcasts.
-
Supabase — handles authentication (GoTrue), PostgreSQL storage for documents/versions, and RLS policies for user isolation.
Docker Desktop only. No Node.js, no npm, nothing else required locally.
-
Clone the repository:
git clone <repo-url> cd collabdocs
-
Create a Supabase project at https://supabase.com:
- Go to SQL Editor and run the contents of
supabase/schema.sql - Copy your Project URL, Anon Key, Service Role Key, and JWT Secret
- Go to SQL Editor and run the contents of
-
Set up environment variables:
cp .env.example .env # Edit .env with your Supabase credentials -
Start the full stack:
docker compose up --build
-
Open the app at http://localhost:5173
Chose Yjs over Operational Transformation because:
- No server-side transform logic — conflicts resolve automatically at the data structure level
- Network partition tolerant — users can edit offline, changes merge on reconnect
- First-class Tiptap integration via
y-prosemirror— Collaboration and CollaborationCursor extensions handle all the wiring - Horizontal scalability — Hocuspocus supports a Redis adapter for multi-instance deployments
Tradeoff: Binary ydoc_state must be persisted to Supabase (as BYTEA) to survive server restarts. The initial payload is slightly larger than delta-based OT, but the complexity savings are substantial.
Hocuspocus implements the full Yjs WebSocket protocol with:
- Authentication hooks (
onAuthenticate) - Document lifecycle (
onLoadDocument,onStoreDocument) - Awareness broadcasting (presence indicators)
- Redis adapter support for horizontal scaling
Rolling a raw ws server would re-implement the same protocol at higher risk.
- Hocuspocus
onChange(3s server-side debounce): persistsydoc_state(binary) + Tiptap JSON todocumentstable and triggers version snapshots. This is the source of truth for collaborative state. - Frontend title debounce (1.5s): title changes go directly to the REST API, independent of content sync.
Content survives WebSocket disconnections because ydoc_state is persisted and reloaded on the next onLoadDocument.
is_deleted flag + deleted_at timestamp, each indexed separately. No data loss — the trash panel queries the soft-deleted set. Hard delete is not exposed in the UI.
createSnapshot compares JSON.stringify of new vs last content — skips write if identical. version_num is sequential per document. Restoring creates a new snapshot rather than mutating history, preserving auditability.
Frontend → Vercel:
- Connect GitHub repository
- Set build command:
cd frontend && npm run build - Set output directory:
frontend/dist - Add all
VITE_environment variables
Backend → Railway or Fly.io:
- Deploy from
backend/Dockerfile(production target) - Set all non-
VITE_environment variables - Expose ports 3001 (REST) and 1234 (WebSocket)
- Offline support —
y-indexeddbprovider to buffer local edits when disconnected, sync queue on reconnect - Document sharing — token-based read-only or editable shareable links
- Redis adapter for Hocuspocus — enables horizontal scaling across multiple backend instances
- Real-time activity feed — Supabase Realtime subscriptions on
document_versionsto show recent edits - Full-text search —
pg_trgmorpgvectorfor semantic document search - Nested document hierarchy —
parent_idondocumentstable for Notion-style nested pages
This project was built using Claude Code (claude-sonnet-4-6) as the primary coding assistant.
Where AI was helpful:
- Scaffolding boilerplate (Dockerfiles, tsconfig, package.json) at speed
- Generating type-safe TypeScript interfaces consistent with the database schema
- Writing the Hocuspocus hook implementations (
onLoadDocument,onStoreDocument,onChange) - SlashCommands extension with tippy.js rendering — the Tiptap Suggestion API has a lot of moving parts
Where AI fell short:
- Initial Hocuspocus
onStoreDocumentused incorrect binary encoding — required manual fix to useY.encodeStateAsUpdatecorrectly - The
CollaborationCursorprovider reference timing (provider initialized in useEffect, but useEditor runs synchronously) required restructuring the Editor component - TypeScript strict mode surfaced several implicit
anytypes in suggestion render callbacks that needed explicit typing
Decisions where AI output was overridden:
- AI initially proposed using
useEffectfor provider initialization insideuseEditoroptions — overridden to useuseRef+ separateuseEffectto avoid stale closures - AI suggested
window.localStoragefor token caching — overridden to always use Supabase session directly to avoid stale tokens - Version restore initially mutated the existing snapshot row — overridden to always insert a new snapshot to preserve history integrity