A full-stack Notion clone built with Next.js 14, featuring a real-time block-based editor, recursive document tree, authentication, file uploads, and publishing.
- Real-time Block Editor - Notion-style block editor powered by BlockNote with slash commands, drag-and-drop blocks, and rich text formatting (bold, italic, underline, strikethrough, code)
- Recursive Document Tree - Infinite nesting of documents with expandable/collapsible sidebar navigation
- Authentication - Secure user authentication via Clerk with sign-in/sign-up flows
- Real-time Database - Convex backend-as-a-service for real-time data synchronization
- File Uploads - Cover image uploads and in-editor image uploads via EdgeStore
- Publishing - Publish documents to a shareable public URL
- Trash & Restore - Soft-delete documents to trash with restore and permanent delete
- Emoji Icons - Add emoji icons to documents via emoji-mart picker
- Cover Images - Add, change, or remove cover images on documents
- Dark Mode - Full dark mode support with next-themes
- Command Palette - Cmd+K search across all documents using cmdk
- Responsive Design - Mobile-friendly collapsible sidebar
- Resizable Sidebar - Drag to resize the sidebar width
| Technology | Purpose |
|---|---|
| Next.js 14 | React framework (App Router) |
| TypeScript | Type safety |
| Tailwind CSS | Utility-first styling |
| Convex | Real-time backend & database |
| Clerk | Authentication |
| EdgeStore | File uploads |
| BlockNote | Block-based editor |
| shadcn/ui | UI component library |
| Radix UI | Headless UI primitives |
| Zustand | State management |
| cmdk | Command palette |
| emoji-mart | Emoji picker |
| sonner | Toast notifications |
| next-themes | Dark mode |
| Lucide React | Icons |
git clone <repository-url>
cd notion-clonenpm installCopy the example env file and fill in your credentials:
cp .env.example .env.localRequired environment variables:
| Variable | Description |
|---|---|
CONVEX_DEPLOYMENT |
Your Convex deployment ID |
NEXT_PUBLIC_CONVEX_URL |
Your Convex deployment URL |
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY |
Clerk publishable key |
CLERK_SECRET_KEY |
Clerk secret key |
EDGE_STORE_ACCESS_KEY |
EdgeStore access key |
EDGE_STORE_SECRET_KEY |
EdgeStore secret key |
npx convex devThis will push the schema and functions to your Convex deployment.
In your Clerk dashboard:
- Go to JWT Templates
- Create a new template for Convex
- Set the issuer domain in
convex/auth.config.js
npm run devOpen http://localhost:3000 in your browser.
notion-clone/
+-- app/
| +-- (main)/ # Authenticated app routes
| | +-- _components/ # Sidebar & navigation components
| | | +-- banner.tsx # Archive banner
| | | +-- confirm-modal.tsx # Confirmation dialog
| | | +-- document-list.tsx # Recursive document tree
| | | +-- item.tsx # Sidebar item
| | | +-- menu.tsx # Document menu dropdown
| | | +-- navbar.tsx # Top navbar
| | | +-- navigation.tsx # Main sidebar
| | | +-- publish.tsx # Publish popover
| | | +-- title.tsx # Editable title
| | | +-- trash-box.tsx # Trash panel
| | | +-- user-item.tsx # User profile
| | +-- (routes)/
| | +-- documents/
| | +-- page.tsx # Documents list
| | +-- [documentId]/
| | +-- page.tsx # Document editor page
| +-- (marketing)/ # Public marketing pages
| | +-- _components/
| | | +-- footer.tsx
| | | +-- heading.tsx
| | | +-- heroes.tsx
| | | +-- logo.tsx
| | | +-- navbar.tsx
| | +-- layout.tsx
| | +-- page.tsx
| +-- (public)/ # Published document preview
| | +-- (routes)/
| | +-- preview/
| | +-- [documentId]/
| | +-- page.tsx
| +-- api/
| | +-- edgestore/
| | +-- [...edgestore]/
| | +-- route.ts # EdgeStore API route
| +-- error.tsx
| +-- globals.css
| +-- layout.tsx
+-- components/
| +-- modals/
| | +-- cover-image-modal.tsx
| | +-- settings-modal.tsx
| +-- providers/
| | +-- convex-provider.tsx
| | +-- modal-provider.tsx
| | +-- theme-provider.tsx
| +-- ui/ # shadcn/ui components
| | +-- alert-dialog.tsx
| | +-- avatar.tsx
| | +-- button.tsx
| | +-- command.tsx
| | +-- dialog.tsx
| | +-- dropdown-menu.tsx
| | +-- input.tsx
| | +-- label.tsx
| | +-- popover.tsx
| | +-- skeleton.tsx
| +-- cover.tsx
| +-- editor.tsx # BlockNote editor wrapper
| +-- icon-picker.tsx
| +-- mode-toggle.tsx
| +-- search-command.tsx
| +-- single-image-dropzone.tsx
| +-- spinner.tsx
| +-- toolbar.tsx
+-- convex/
| +-- auth.config.js
| +-- documents.ts # All mutations & queries
| +-- schema.ts # Database schema
+-- hooks/
| +-- use-cover-image.ts
| +-- use-origin.ts
| +-- use-scroll-top.ts
| +-- use-search.ts
| +-- use-settings.ts
+-- lib/
| +-- edgestore.ts
| +-- utils.ts
+-- .env.example
+-- .gitignore
+-- components.json
+-- next.config.js
+-- package.json
+-- postcss.config.js
+-- tailwind.config.ts
+-- tsconfig.json
The Convex database has a single documents table:
| Field | Type | Description |
|---|---|---|
title |
string |
Document title |
userId |
string |
Owner user ID (from Clerk) |
isArchived |
boolean |
Whether document is in trash |
parentDocument |
Id<"documents">? |
Optional parent for nesting |
content |
string? |
BlockNote JSON content |
coverImage |
string? |
Cover image URL |
icon |
string? |
Emoji icon |
isPublished |
boolean |
Whether publicly accessible |
Indexes:
by_user- Query documents by userIdby_user_parent- Query documents by userId + parentDocument (for sidebar tree)
getSidebar- Get documents for sidebar tree (by parent)getById- Get single document (auth-checked)getByIdPublic- Get published document (no auth)getTrash- Get all archived documentsgetSearch- Get all non-archived documents for search
create- Create new documentupdate- Update document fieldsarchive- Recursively archive document and childrenrestore- Restore document from trashremove- Permanently delete documentremoveIcon- Remove emoji iconremoveCoverImage- Remove cover image
This project is for educational purposes. Feel free to use it as a reference or starting point for your own projects.