A real-time collaborative web code editor built with Next.js, PartyKit, and Yjs CRDT for conflict-free synchronization. Multiple users can simultaneously edit HTML, CSS, and JavaScript files in separate rooms with live preview and user presence tracking.
- Real-time Collaboration - Multiple users can edit simultaneously with sub-100ms latency
- Monaco Editor - Full VS Code editor experience with syntax highlighting, IntelliSense, and keyboard shortcuts
- Multi-File Support - Edit HTML, CSS, and JavaScript in separate tabs
- Live Preview - See your changes instantly in an integrated preview pane
- User Presence - Track who's in the room with colored avatars and cursors
- Auto-Save - Automatic persistence to database every 3 seconds
- CRDT Synchronization - Conflict-free editing powered by Yjs
- Room Isolation - Each room is completely separate with unique identifiers
- Auto-Reconnect - Seamless recovery from network interruptions
- Next.js 16.0.3 with App Router and React 19.2.0
- Monaco Editor - VS Code's editor engine
- Yjs - CRDT for real-time synchronization
- y-monaco - Monaco bindings for Yjs
- Tailwind CSS v4 - Utility-first styling
- Radix UI - Accessible component primitives
- Framer Motion - Animation library
- PartyKit - WebSocket server framework (port 1999)
- y-partykit - Yjs provider for PartyKit
- Convex - Database for auto-save persistence
- Bun - Fast JavaScript runtime and package manager
- Bun installed (or Node.js 18+)
- A Convex account (for persistence)
-
Clone the repository
git clone <repository-url> cd coilcode
-
Install backend dependencies
cd partykit bun install -
Install frontend dependencies
cd ../frontend bun install -
Configure environment variables
Create
frontend/.env.local:NEXT_PUBLIC_PARTYKIT_HOST=localhost:1999 NEXT_PUBLIC_APP_URL=http://localhost:3000 CONVEX_DEPLOYMENT=your-deployment-name NEXT_PUBLIC_CONVEX_URL=https://your-deployment.convex.cloud
You'll need three terminal windows:
Terminal 1 - Start PartyKit Server:
cd partykit
bun run devWait for: [pk:inf] Ready on http://0.0.0.0:1999
Terminal 2 - Start Next.js Frontend:
cd frontend
bun run devWait for: ✓ Ready in 2-3s
Terminal 3 - Start Convex (Optional):
cd frontend
bunx convex devOpen your browser to http://localhost:3000 and start collaborating!
- Click "Create New Room"
- Copy the URL (e.g.,
http://localhost:3000/editor/abc123) - Open the same URL in a new tab or browser
- Type in one editor and watch it appear instantly in the other!
coilcode/
├── frontend/
│ ├── app/
│ │ ├── page.tsx # Home page (room creation/join)
│ │ ├── layout.tsx # Root layout with Convex provider
│ │ ├── editor/[roomId]/page.tsx # Main collaborative editor
│ │ ├── simple/page.tsx # Simple editor test page
│ │ └── test/page.tsx # WebSocket debug page
│ ├── components/
│ │ ├── editor/
│ │ │ ├── CollaborativeEditor.tsx # Monaco with Yjs binding
│ │ │ ├── EditorLayout.tsx # 3-panel layout
│ │ │ ├── MultiFileEditor.tsx # Tabbed editor (HTML/CSS/JS)
│ │ │ ├── FileTree.tsx # Left sidebar file browser
│ │ │ ├── PreviewPane.tsx # Live preview iframe
│ │ │ └── UserListTooltip.tsx # User presence UI
│ │ ├── ui/ # Radix UI components
│ │ └── UsernamePrompt.tsx # Username modal
│ ├── hooks/
│ │ ├── useUsername.ts # User identity (localStorage)
│ │ ├── usePresence.ts # Real-time user tracking
│ │ └── useAutoSave.ts # Auto-save to Convex
│ ├── lib/
│ │ ├── yjs-setup.ts # Yjs initialization
│ │ ├── monaco-setup.ts # Monaco configuration
│ │ ├── cursor-colors.ts # Color assignment
│ │ └── ensure-unique-colors.ts # Unique colors per room
│ ├── convex/
│ │ ├── schema.ts # Database schema
│ │ ├── rooms.ts # Room CRUD operations
│ │ └── crons.ts # Scheduled cleanup tasks
│ └── next.config.ts # Webpack + Monaco plugin config
└── partykit/
├── server.ts # Yjs PartyKit server
└── partykit.json # PartyKit config
Client Browser ←→ WebSocket ←→ PartyKit Server (Yjs) ←→ WebSocket ←→ Other Clients
↓ ↓
Yjs Document Yjs Document
↓ ↓
Monaco Binding Monaco Binding
-
Yjs Document - CRDT-based shared data structure with three text types:
ydoc.getText('html')- HTML contentydoc.getText('css')- CSS contentydoc.getText('js')- JavaScript content
-
YPartyKitProvider - WebSocket provider connecting to PartyKit server
-
MonacoBinding - Syncs Monaco editor instances with Yjs text types
-
Awareness Protocol - Tracks user presence, cursor positions, and metadata
Initialization:
- User enters/creates room
- Check user identity from localStorage
- Create Yjs document + PartyKit provider
- Set user awareness (username + avatar)
- Load room content from Convex database
- Apply initial content to Yjs text types
- Render 3-panel editor layout
Real-Time Edits:
- User types in Monaco editor
- MonacoBinding syncs changes to Yjs document
- Yjs broadcasts via PartyKit WebSocket
- Other clients receive and apply changes
- Preview pane updates (debounced)
- Auto-save triggers after 3s of inactivity
bun install # Install dependencies
bun run dev # Start dev server (localhost:3000)
bun run dev:network # Start with network access (0.0.0.0)
bun run dev:all # Start Next.js + Convex concurrently
bun run build # Production build
bun run lint # Run ESLint
bunx convex dev # Start Convex backend separatelybun install # Install dependencies
bun run dev # Start PartyKit server (localhost:1999)
bun run deploy # Deploy to PartyKit CloudUser information is stored in localStorage under the key userInfo:
interface UserInfo {
username: string;
gender: "boy" | "girl" | "random";
}- Deterministic hash-based colors using
clientID + username - 30 predefined distinct colors
- Enforced uniqueness per room
- Colors released when user disconnects
Open the browser console and use these global functions:
window.fullCursorDiagnostics() // Complete diagnostic report
window.quickColorCheckTest() // Quick status check
window.previewCursors() // Visual preview of colors
window.applyCursorColorsNow() // Force apply colors
window.watchCursorChanges() // Real-time monitoringScenario 1: Two-Tab Test
1. Tab 1: Create room
2. Tab 2: Open same URL
3. Tab 1: Type "function hello() {"
4. Tab 2: Should see it instantly
5. Both tabs stay synchronized
Scenario 2: Different Browsers
1. Chrome: http://localhost:3000/editor/test123
2. Firefox: http://localhost:3000/editor/test123
3. Type in Chrome → See in Firefox
4. Type in Firefox → See in Chrome
Scenario 3: Connection Recovery
1. Open editor room
2. Stop PartyKit server (Ctrl+C)
3. Type in editor (works locally)
4. Restart PartyKit server
5. Connection auto-recovers
6. Changes sync automatically
Scenario 4: Room Isolation
1. Room A: /editor/room-a
2. Room B: /editor/room-b
3. Type "AAA" in Room A
4. Type "BBB" in Room B
5. Verify rooms remain isolated
Visit http://localhost:3000/test to check:
- Connection status (should be green)
- WebSocket URL
- Document content
- Debug information
- Uses webpack mode (not Turbopack) for Monaco compatibility
- MonacoWebpackPlugin with languages:
["html", "css", "javascript", "typescript", "json"] - Worker
publicPath: "/_next/"to match Next.js asset paths - WebAssembly support enabled
- Client-side only (no SSR)
Dynamic Import Pattern:
const CollaborativeEditor = dynamic(
() => import('@/components/editor/CollaborativeEditor'),
{ ssr: false }
);rooms: {
roomId: string // Unique room identifier
htmlContent?: string // HTML content
cssContent?: string // CSS content
jsContent?: string // JavaScript content
lastEditedBy: string // Last editor's username
lastEditedAt: number // Timestamp of last edit
lastActiveAt?: number // For cleanup cron jobs
saveCount?: number // Analytics
totalSize?: number // Analytics
createdAt?: number // Analytics
}cd frontend
vercel deploycd partykit
bun run deployUpdate your .env.local:
NEXT_PUBLIC_PARTYKIT_HOST=your-app.partykit.dev
NEXT_PUBLIC_APP_URL=https://your-app.vercel.appSolution:
# Ensure PartyKit is running
cd partykit
bun run devWindows:
# Kill port 1999
netstat -ano | findstr :1999
taskkill /PID [PID] /F
# Kill port 3000
netstat -ano | findstr :3000
taskkill /PID [PID] /FmacOS/Linux:
# Kill port 1999
lsof -ti:1999 | xargs kill -9
# Kill port 3000
lsof -ti:3000 | xargs kill -9Solution:
cd frontend
rm -rf .next node_modules
bun install
bun run devCheck:
- Both tabs have the SAME room ID in URL
- DevTools → Network → WS shows WebSocket connection
- PartyKit server is running
- No console errors
- No visual cursor indicators in Monaco (awareness data exists but not rendered in editor)
- No authentication/authorization (trust-based username system)
- No room management UI
- No chat functionality
- No version history
- Single language support (HTML/CSS/JS only, no TypeScript/Python selector)
- Preview sandbox has no restrictions (full JS execution)
- START_HERE.md - Quick start guide
- QUICKSTART.md - Fast setup
- IMPLEMENTATION_STATUS.md - Feature status
- TESTING_GUIDE.md - Comprehensive testing
- CLAUDE.md - AI assistant instructions
- VISUAL_TESTING_GUIDE.md - Visual testing
- CURSOR_VERIFICATION_CHECKLIST.md - Cursor system tests
This is a personal project, but contributions are welcome! Please feel free to:
- Report bugs
- Suggest features
- Submit pull requests
MIT License - See LICENSE file for details
Built with these amazing technologies:
- Next.js - React framework
- PartyKit - Real-time infrastructure
- Yjs - CRDT library
- Monaco Editor - VS Code editor
- Convex - Backend platform
- Tailwind CSS - CSS framework
- Radix UI - UI components
Happy Collaborative Coding! 💻✨
