Architecture
MarkDocs is a full-stack collaborative markdown editor.
Stack
| Layer | Technology |
|---|---|
| Frontend | Next.js, React, Tailwind CSS, shadcn/ui |
| Editor | CodeMirror 6, Yjs, WebSockets |
| Backend | Next.js API routes, custom WebSocket server |
| Database | PostgreSQL via Prisma |
| Auth | Handle-based with bcrypt + JWT (jose), API keys |
| CLI | Commander.js, chalk, ora |
| MCP Server | @modelcontextprotocol/sdk (stdio) |
| Deployment | Docker Compose |
Project Structure
markdocs/
├── src/
│ ├── app/ # Next.js pages and API routes
│ │ ├── api/
│ │ │ ├── auth/ # Login, signup, logout
│ │ │ ├── documents/ # CRUD, content, edit, collaborators, history
│ │ │ ├── comments/ # Reply, resolve, unresolve, delete
│ │ │ ├── suggestions/ # Accept, reject
│ │ │ ├── keys/ # API key management
│ │ │ ├── invites/ # Invite link generation
│ │ │ └── users/ # User listing
│ │ ├── dashboard/ # Document list
│ │ ├── doc/[id]/ # Collaborative editor
│ │ └── settings/ # API keys, invites, profile
│ ├── components/ # React components
│ └── lib/ # Shared utilities, auth, prisma, yjs
├── cli/
│ └── src/
│ ├── index.ts # CLI commands (Commander)
│ ├── mcp.ts # MCP server (23 tools, 2 resources)
│ ├── client.ts # HTTP client for the API
│ └── config.ts # ~/.markdocs/config.json management
├── prisma/
│ └── schema.prisma # Database schema
├── server.ts # Custom server (Next.js + WebSocket)
├── Dockerfile # App container
└── docker-compose.yml # Full stack (app + postgres)Auth
Three auth methods, resolved in priority order:
1. MARKDOCS_API_KEY env var → Bearer token
2. ~/.markdocs/config.json → Saved API key or JWT
3. Session cookie → Browser sessionsThe CLI login command authenticates with handle/password, receives a JWT, and saves it locally. The setup command uses that JWT to create a persistent API key. All CLI and MCP calls then use the API key automatically.
API routes use getAuth() which checks Bearer tokens first (as API key hash, then as JWT), falling back to session cookies for browser clients.
Data Flow
Real-Time Editing (Browser)
Browser A ──► WebSocket ──► Yjs Server ──► WebSocket ──► Browser B
│
Snapshot to DB
(on disconnect)Documents use Yjs CRDTs for conflict-free real-time editing. The browser editor is CodeMirror 6 with y-codemirror.next for collaborative binding. Changes sync over WebSockets and persist as Yjs state snapshots to PostgreSQL.
CLI and MCP (Agents)
Agent / CLI
│
markdocs <command>
│
▼
HTTP + API Key ──► API Routes ──► Prisma ──► PostgreSQL
│
Yjs snapshot
(read/write)AI Agent ◄── stdio ──► markdocs mcp ── HTTP ──► MarkDocs APIThe CLI and MCP server share the same HTTP client and credential store (~/.markdocs/config.json). The MCP server translates JSON-RPC tool calls over stdio into REST API requests.
Text-Based Edit API
The /api/documents/:id/edit endpoint lets agents edit by quoting visible text instead of counting character positions. The server reads the current Yjs document, resolves text targets to positions, applies content operations atomically, then applies review operations (comments, suggestions) in order.
Agent sends: { "op": "replace", "find": "old text", "replace": "new text" }
│
Server reads Yjs snapshot
Finds "old text" at position 42
Replaces via Yjs
│
Returns updated document contentDatabase Schema
Key models:
- User — handle, passwordHash, name, avatarUrl
- Document — title, visibility, creatorId, orgId
- DocumentCollaborator — userId, documentId, role (editor/viewer)
- DocumentSnapshot — Yjs binary state for a document
- Comment — content, fromPos, toPos, parentId (threaded replies), resolved, resolvedBy
- Suggestion — originalText, suggestedText, fromPos, toPos, status (pending/accepted/rejected)
- EditHistory — action, diff, source (web/api/mcp), metadata
- ApiKey — keyHash, prefix, userId, expiresAt, revokedAt
- Invite — code, createdBy, claimedBy, expiresAt