A collaborative Markdown editor with a block-based WYSIWYG interface, file browser, and multi-tab support. Built with React, BlockNote, Express, and TypeScript.
docker compose up --buildOpen http://localhost:3000 in your browser.
The first run seeds a docs volume with sample Markdown files. Documents persist across container restarts.
This project uses a VS Code Devcontainer. The .devcontainer/Dockerfile installs Node.js 20 LTS, and the postCreateCommand in devcontainer.json automatically installs dependencies to /tmp/collab-editor-deps (Docker filesystem) for performance, then symlinks node_modules back to the workspace.
Once the devcontainer is built, dependencies are ready:
npm run devThis starts two processes concurrently:
- Express server on port 3000 (via
tsx watch) - Vite dev server on port 5173 (with API proxy to 3000)
Open http://localhost:5173 for hot-reloading development.
By default, the server reads/writes Markdown files in ./docs. Override with the DOCS_PATH environment variable.
The devcontainer workspace directory is bind-mounted from the host, which is very slow for the thousands of small files in node_modules. Dependencies are installed on the Docker filesystem (/tmp/collab-editor-deps) and symlinked into the workspace. If the container is rebuilt, the postCreateCommand re-runs automatically.
To manually re-install after changing package.json:
cp package.json package-lock.json /tmp/collab-editor-deps/
cd /tmp/collab-editor-deps && npm installRequires Node.js 20+.
npm install
npm run devnpm run build
npm startThis:
- Bundles the React client with Vite →
dist/client/ - Compiles the server TypeScript →
dist/server/ - Runs the Express server serving both the API and static client
| File | Purpose |
|---|---|
Dockerfile |
Multi-stage build: builder (install + build) → production (runtime only) |
docker-compose.yml |
Single app service on port 3000 with a named docs volume |
docker-entrypoint.sh |
Seeds the docs volume with sample files if empty on first run |
.env |
DOCS_PATH=/app/docs, PORT=3000 |
docker compose up --builddocker compose down -v
docker compose up --builddocker compose logs -f app| Variable | Default | Purpose |
|---|---|---|
PORT |
3000 |
Server listening port |
DOCS_PATH |
/app/docs (Docker) or ./docs (local) |
Directory containing Markdown files |
├── .devcontainer/
│ ├── Dockerfile # Devcontainer: Ubuntu + Node.js 20 + Docker CLI
│ └── devcontainer.json # Devcontainer config with /tmp node_modules setup
├── Dockerfile # Multi-stage Docker build (production)
├── docker-compose.yml # Single-service compose
├── docker-entrypoint.sh # Volume seeder
├── package.json # Dependencies and scripts
├── tsconfig.json # Client TypeScript config
├── tsconfig.server.json # Server TypeScript config
├── vite.config.ts # Vite bundler + dev proxy
├── .env # Environment variables
├── docs/ # Sample documents (mounted volume in Docker)
│ ├── AI-COLLABORATION.md # Full requirements and build log
│ ├── welcome.md
│ └── example/
│ └── nested-doc.md
├── plans/ # Implementation plans
│ ├── phase-1a-plan.md
│ └── source-mode-plan.md
└── src/
├── server/
│ ├── index.ts # Express entry point
│ ├── routes/files.ts # File CRUD REST API
│ └── services/fileService.ts # Filesystem ops with path traversal protection
└── client/
├── index.html # SPA entry
├── main.tsx # React entry with MantineProvider
├── App.tsx # Three-panel layout
├── styles/global.css # All component styles
├── hooks/
│ ├── useFileTree.ts # File tree data fetching
│ └── useOpenFiles.ts # Open tabs state management
└── components/
├── Editor/
│ ├── MarkdownEditor.tsx # BlockNote + Source mode toggle
│ ├── EditorPanel.tsx # Tab bar + active editor
│ ├── TabBar.tsx # Tab bar component
│ └── Tab.tsx # Single tab with dirty indicator
├── FileBrowser/
│ ├── FileBrowser.tsx # Tree view sidebar
│ ├── FileTreeItem.tsx # Recursive tree node
│ └── NewFileDialog.tsx # Create file/folder dialog
└── ChatPanel/
└── ChatPanel.tsx # Placeholder (Phase 1c)
The editor supports two modes, toggled per-tab via the Rich / Source buttons:
- Rich mode (default): BlockNote WYSIWYG block editor
- Source mode: Raw Markdown in a monospace textarea
Content syncs bidirectionally on toggle. Both modes trigger the same save/dirty-tracking flow.
| Method | Path | Body | Response |
|---|---|---|---|
GET |
/api/tree |
— | FileTreeNode[] |
GET |
/api/files/*path |
— | { content: string } |
PUT |
/api/files/*path |
{ content: string } |
{ success: true } |
POST |
/api/files/*path |
{ type: "file" | "dir" } |
{ success: true } |
PATCH |
/api/files/*path |
{ newPath: string } |
{ success: true } |
DELETE |
/api/files/*path |
— | { success: true } |
All file paths are validated to prevent path traversal attacks.
| Shortcut | Action |
|---|---|
Ctrl+S / Cmd+S |
Save active document |
Tab (source mode) |
Insert 2 spaces |
See the full requirements and build phases in docs/AI-COLLABORATION.md.
| Phase | Status | Description |
|---|---|---|
| 1a | ✅ Done | Single-user editor in Docker |
| 1b | Planned | Real-time collaboration (Y.js + WebSocket) |
| 1c | Planned | AI chat integration |
| 2 | Planned | Git integration & polish |