A collaborative document editor built with Next.js App Router, Tiptap, Convex, and Clerk. The realtime editing model is CRDT-based (Yjs-style updates) with presence, comments, and notifications layered on top.
- Overview
- Features
- Tech stack
- Architecture
- CRDT + Yjs collaboration model
- Editor and UI composition
- Convex + Clerk: data, auth, and tenancy
- Data model
- Document lifecycle and discovery
- Metrics and observability
- Setup
- Running locally
- Scripts
- Project layout
- Troubleshooting
This project is a Google Docs-style editor that supports authenticated, multi-user editing with rich text tools, document templates, organization-aware sharing, and realtime collaboration features like presence and threaded comments. It uses Convex for persistent data and server functions, and Clerk for authentication and organization identity. The collaboration layer is designed around CRDTs (specifically the Yjs mental model) to ensure conflict-free merges under concurrent edits.
- Rich text editing with Tiptap (tables, tasks, images, links, highlights, text styles)
- Document templates gallery (blank, proposal, resume, letters)
- Document list with search and pagination
- Realtime collaboration with presence avatars and comment threads
- Notifications inbox for comment activity
- Organization-aware access control (owner or org member)
- Server-authenticated collaboration sessions
- UI: Next.js 15 App Router, React, Tailwind CSS, Radix UI
- Editor: Tiptap + custom extensions
- Realtime model: CRDTs (Yjs-style updates and awareness)
- Backend: Convex (queries, mutations, schema, indexes)
- Auth: Clerk (JWT templates, org membership, middleware)
High-level view:
+----------------------+ +-----------------------+
| Browser (Next.js) |<---->| Next.js App Router |
| - Tiptap editor | | - Pages & layouts |
| - Presence UI | | - API routes |
+----------+-----------+ +-----------+-----------+
| |
| JWT + session | queries/mutations
v v
+---------+ +-----------+
| Clerk | | Convex |
| Auth | | DB + API |
+----+----+ +-----+-----+
| |
| realtime auth |
v |
+------------------------------+ |
| CRDT sync provider (Yjs- |<-----+
| compatible) |
| - document updates |
| - awareness/presence |
| - threads/inbox |
+------------------------------+
Key flows:
- The browser loads Next.js routes, which preload data from Convex.
- Clerk authenticates users and provides org membership data.
- Convex stores document metadata and enforces access control.
- A realtime provider streams CRDT updates so multiple editors converge.
- The document ID from the route is used as the collaboration room ID.
This project treats collaboration as a CRDT problem. The editor state is modeled as a shared document that can be updated concurrently by multiple clients. Each client emits incremental updates, and all replicas converge without conflicts.
Core ideas (Yjs-style):
- Shared document: The editor state is represented as a shared data structure (Y.Doc in Yjs terms).
- Incremental updates: Each change produces a small update payload rather than a full document snapshot.
- Conflict-free merges: Updates are commutative and associative, so order does not matter.
- Awareness: Presence data (cursor/selection, user identity) is broadcast separately from document updates.
- Snapshots/compaction: Over time, updates can be merged or snapshotted to keep history manageable.
Yjs internals (short, practical view):
- State vectors: Each client tracks a version vector to compute minimal diffs during sync.
- Binary updates: Changes are encoded as compact binary updates that can be merged and rebroadcast.
- Structs + delete sets: Inserts/formatting are encoded as structs, deletes are tracked separately, and the two reconcile on apply.
- Awareness protocol: Presence is ephemeral and not persisted; it uses a lightweight protocol (
y-protocols) distinct from document updates.
Collaboration dataflow (conceptual):
User A edit
|
v
[CRDT update] ---> broadcast ---> other clients
| |
| apply update
v v
local state converged state
In this repo, the collaboration extension integrates with the editor and a realtime provider is used to relay Yjs-style updates and awareness across sessions. The provider also supports threads and inbox notifications used by the comments UI.
The document view is composed of several cooperating pieces:
- Layout composition:
src/app/documents/[documentId]/document.tsxcomposesNavbar,Toolbar, andEditorinside aRoomprovider. - Realtime room wiring:
src/app/documents/[documentId]/room.tsxcreates the collaboration room and resolves users and documents for mentions and room info. - Editor instance:
src/app/documents/[documentId]/editor.tsxconfigures Tiptap with rich-text extensions and connects it to the CRDT provider. - Toolbar commands:
src/app/documents/[documentId]/toolbar.tsxreads the editor instance from a shared store to run formatting commands. - State sharing:
src/store/use-editor-store.tsholds the Tiptap editor instance so the toolbar and editor stay in sync. - Presence + comments:
src/app/documents/[documentId]/avatar.tsxrenders collaborator avatars;threads.tsxandinbox.tsxrender comment threads and notifications.
Convex and Clerk are tightly integrated:
- Clerk provides authentication, session claims, and organization membership.
- Convex stores document metadata and performs queries/mutations with auth checks.
- Next.js uses Clerk server utilities to mint a Convex JWT (
template: "convex") for secure preloading.
Where this happens in code:
src/components/convex-client-provider.tsxwires Clerk auth into Convex React client.src/middleware.tsenforces authentication at the edge.src/app/documents/[documentId]/page.tsxpreloads document data using a Clerk-issued Convex token.convex/documents.tsenforces ownership/org checks before reads and writes.
Access control logic (simplified):
request -> Clerk session -> Convex query/mutation
| |
| check owner or org
v v
allow/deny result
Realtime session authorization (used before joining a collaborative room):
Editor -> POST /api/liveblocks-auth
| |
| v
| Clerk session + Convex doc check
| |
v v
token ok 401/403
|
v
join room + start CRDT sync
Convex schema (see convex/schema.ts):
- documents
title: stringinitialContent: optional stringownerId: string (Clerk user id)organizationId: optional string- indexes:
by_owner_idby_organization_idsearch_title(full-text search on title)
Document list queries use the search index for fast title filtering and support pagination (convex/documents.ts).
Note on content storage:
- Convex stores document metadata and optional template content.
- The live collaborative document body is managed by the CRDT layer; persistence strategy can be extended if you want to sync snapshots into Convex.
Creation and discovery flow:
Template click or \"Blank\" -> Convex mutation -> documentId
| |
v v
navigate to /documents/:id preload document metadata
| |
v v
join CRDT room render editor + toolbar
Search and pagination details:
src/app/(home)/page.tsxusesusePaginatedQueryto load document pages.src/app/(home)/search-input.tsxandsrc/hooks/use-search-param.tskeep search terms in the URL.convex/documents.tsroutes search to thesearch_titleindex for fast filtering.src/constants/template.tsandsrc/app/(home)/template-gallery.tsxdefine and render the template gallery.
This repo does not ship a full metrics pipeline, but it is ready for instrumentation. Suggested metrics by layer:
- Editor/CRDT
- Update payload size (bytes) and frequency
- Collaboration latency (edit -> remote apply)
- Awareness/presence fan-out (active collaborators per doc)
- Product
- Documents created per day
- Avg. collaborators per document
- Comment threads created/resolved
- Backend (Convex)
- Query/mutation latency and error rate
- Search latency and pagination depth
- Authorization failure counts
- Auth (Clerk)
- Sign-in success rate
- Org membership changes
Where to observe:
- Convex dashboard provides function logs and performance stats.
- Clerk dashboard provides authentication activity and user/org analytics.
- Client metrics can be shipped to your preferred analytics or APM tool.
- Node.js 18+ (or the version supported by your Next.js setup)
- A Clerk application
- A Convex project
- A realtime provider key for CRDT sync
npm install
- Create a Clerk application.
- Create a JWT template named
convex. - Update
convex/auth.config.tswith your Clerk instance domain. - Add your keys to
.env.local(see below).
- Run
npx convex devand follow the prompts to create or link a project. - Convex will generate
.env.localentries likeCONVEX_DEPLOYMENTandNEXT_PUBLIC_CONVEX_URL. - Keep those entries; they are required by the client and server.
Set the secret key for the CRDT provider in .env.local. The Next.js auth route uses it to authorize collaboration sessions.
Create .env.local in the project root with:
# Convex
CONVEX_DEPLOYMENT=your-convex-deployment
NEXT_PUBLIC_CONVEX_URL=https://your-convex-url
# Clerk
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_...
CLERK_SECRET_KEY=sk_...
# Realtime collaboration (CRDT provider)
LIVEBLOCKS_SECRET_KEY=sk_liveblocks_...
Notes:
- The Clerk JWT template must be named
convexto matchgetToken({ template: "convex" }). - The auth config in
convex/auth.config.tsmust match your Clerk domain.
In one terminal, start Convex:
npx convex dev
In another terminal, start Next.js:
npm run dev
Then open http://localhost:3000.
npm run dev- start Next.js dev servernpm run build- build production bundlenpm run start- start production servernpm run lint- lint with Next.js rules
src/app/(home)- landing page, templates, document listsrc/app/documents/[documentId]- editor, toolbar, room, commentssrc/app/api/liveblocks-auth/route.ts- realtime session authorizationsrc/components/convex-client-provider.tsx- Clerk + Convex client wiringconvex/- schema, queries, and mutationspublic/- template thumbnails and assets
- Unauthorized errors: verify Clerk keys and JWT template name
convex. - No documents loading: confirm
NEXT_PUBLIC_CONVEX_URLand Convex dev server are running. - Collaboration not syncing: check
LIVEBLOCKS_SECRET_KEYand the auth route response. - Org sharing not working: ensure users belong to the same Clerk organization.