Skip to content

Commit f814ea0

Browse files
committed
Updated readme and docs
1 parent a31da4f commit f814ea0

2 files changed

Lines changed: 265 additions & 248 deletions

File tree

README.md

Lines changed: 59 additions & 248 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,60 @@
1-
# Markdoc - Ephemeral Real-Time Collaboration Engine
1+
# Markdoc
22

3-
A **database-free**, **in-memory**, **real-time collaborative document editor** built with Elixir/Phoenix and React. Documents exist only while users are actively collaborating and automatically self-destruct after inactivity.
3+
A **database-free**, **in-memory**, **real-time collaborative document editor** where documents exist only while users are actively collaborating and automatically self-destruct after inactivity.
44

5-
## 🌟 Features
5+
## What is Markdoc?
66

7-
- **Real-Time Collaboration**: Multiple users can edit documents simultaneously with instant synchronization
8-
- **Ephemeral Architecture**: No database - all state lives in memory and disappears when idle
9-
- **CRDT-Based**: Conflict-free replicated data types (Y.js) ensure consistency
10-
- **Memory Efficient**: Client-side snapshotting automatically compresses document history
11-
- **User Awareness**: See who's online with colored indicators
12-
- **Scalable**: Built on BEAM/OTP with Phoenix PubSub for horizontal scaling
13-
- **Block-Based Editor**: Notion-like editing experience powered by BlockNote
7+
Markdoc is an ephemeral collaboration engine that demonstrates how to build real-time collaborative applications without traditional database persistence. Documents live entirely in memory using CRDTs (Conflict-free Replicated Data Types) for conflict-free synchronization across multiple users.
148

15-
## 🏗️ Architecture
9+
**Key Characteristics:**
10+
- No database - all state exists in memory
11+
- Real-time collaborative editing with instant synchronization
12+
- Automatic cleanup - documents disappear after 1 hour of inactivity
13+
- CRDT-based consistency using Y.js
14+
- Built on the BEAM VM for fault tolerance and scalability
1615

17-
### Backend (Elixir/Phoenix)
18-
- **Phoenix Channels**: WebSocket transport for real-time sync
19-
- **GenServer per Document**: Each active document is a lightweight process
20-
- **DynamicSupervisor**: Spawns document processes on-demand
21-
- **Phoenix Presence**: Tracks online users with CRDT-based state
22-
- **Registry**: Process naming and discovery
23-
- **Automatic Cleanup**: Documents terminate after 1 hour of inactivity
16+
## Technologies Used
2417

25-
### Frontend (React/TypeScript)
26-
- **BlockNote**: Rich block-based editor
27-
- **Y.js**: CRDT engine for conflict-free merging
28-
- **Phoenix Client**: WebSocket communication
29-
- **Custom Provider**: Bridges Y.js and Phoenix Channels
18+
**Backend:**
19+
- **Elixir 1.18+** / **Erlang/OTP 27+** - Concurrent process model
20+
- **Phoenix Framework** - WebSocket channels and PubSub
21+
- **GenServer** - Document state management
22+
- **Phoenix Presence** - User awareness tracking
3023

31-
### Data Flow
32-
1. User joins document → Backend spawns GenServer if needed
33-
2. User types → Y.js creates update → Sent to server via WebSocket
34-
3. Server broadcasts update → Other clients apply via Y.js
35-
4. After 50 updates → Server requests snapshot → Client compresses & sends back
36-
5. Last user leaves → 1-hour timer starts → Process terminates
24+
**Frontend:**
25+
- **React** + **TypeScript** - UI framework
26+
- **BlockNote** - Block-based rich text editor (Notion-like)
27+
- **Y.js** - CRDT engine for conflict-free merging
28+
- **Phoenix Client** - WebSocket communication
3729

38-
## 📋 Prerequisites
30+
## Quick Setup
3931

40-
- **Elixir** 1.18+ and **Erlang/OTP** 27+
41-
- **Node.js** 20+
42-
- **npm** or **yarn**
32+
### Prerequisites
4333

44-
## 🚀 Quick Start
34+
- Elixir 1.18+ and Erlang/OTP 27+
35+
- Node.js 20+
36+
- npm or yarn
4537

46-
### 1. Install Dependencies
38+
### Installation
4739

48-
**Backend:**
40+
1. **Clone and install backend dependencies:**
4941
```bash
42+
git clone <repository-url>
43+
cd markdoc
5044
mix deps.get
5145
mix compile
5246
```
5347

54-
**Frontend:**
48+
2. **Install frontend dependencies:**
5549
```bash
5650
cd frontend
5751
npm install
5852
cd ..
5953
```
6054

61-
### 2. Start Development Servers
55+
### Running the Application
56+
57+
Start both servers in separate terminals:
6258

6359
**Terminal 1 - Backend:**
6460
```bash
@@ -71,232 +67,47 @@ cd frontend
7167
npm run dev
7268
```
7369

74-
### 3. Open in Browser
75-
70+
**Access the application:**
7671
- Frontend: http://localhost:5173
7772
- Backend: http://localhost:4000
7873

79-
To collaborate on the same document, open **multiple browser tabs** with the same URL.
74+
Open multiple browser tabs with the same URL to test real-time collaboration.
75+
76+
## Documentation
77+
78+
For detailed information, see the docs directory:
8079

81-
## 📂 Project Structure
80+
- **[Architecture](docs/arch_doc.md)** - System design and component details
81+
- **[Client-Side Snapshotting](docs/client-side-snapshotting.md)** - Memory optimization strategy
82+
- **[Configuration](docs/configuration.md)** - Customization options
83+
- **[Testing](docs/testing.md)** - Testing strategies and examples
84+
- **[Deployment](docs/deployment.md)** - Production deployment guide
85+
- **[Security](docs/security.md)** - Security considerations and best practices
86+
87+
## Project Structure
8288

8389
```
8490
markdoc/
8591
├── lib/
8692
│ ├── markdoc/
87-
│ │ ├── application.ex # Supervision tree
88-
│ │ ├── doc_registry.ex # Process registry wrapper
89-
│ │ ├── doc_supervisor.ex # Dynamic supervisor
90-
│ │ └── doc_server.ex # Document GenServer ⭐
93+
│ │ ├── doc_server.ex # Document GenServer
94+
│ │ └── doc_supervisor.ex # Dynamic supervisor
9195
│ └── markdoc_web/
92-
│ ├── channels/
93-
│ │ ├── user_socket.ex # WebSocket handler
94-
│ │ ├── doc_channel.ex # Document channel ⭐
95-
│ │ └── presence.ex # Presence tracking
96-
│ ├── endpoint.ex # Phoenix endpoint
97-
│ └── router.ex # HTTP routes
96+
│ └── channels/
97+
│ └── doc_channel.ex # WebSocket channel
9898
├── frontend/
9999
│ └── src/
100100
│ ├── lib/
101-
│ │ └── PhoenixProvider.ts # Y.js ↔ Phoenix bridge ⭐
102-
│ ├── components/
103-
│ │ ├── Editor.tsx # Main editor component ⭐
104-
│ │ ├── UserPresence.tsx # Online users indicator
105-
│ │ └── ConnectionStatus.tsx # WebSocket status
106-
│ ├── hooks/
107-
│ │ └── usePresence.ts # Presence hook
108-
│ └── App.tsx # Root component
109-
└── docs/
110-
├── arch_doc.md # Architecture documentation
111-
└── client-side-snapshotting.md # Snapshotting strategy
112-
```
113-
114-
## 🔧 Configuration
115-
116-
### Change Snapshot Threshold
117-
118-
Edit `lib/markdoc/doc_server.ex`:
119-
```elixir
120-
@snapshot_threshold 50 # Trigger snapshot after 50 updates
121-
```
122-
123-
### Change Cleanup Timeout
124-
125-
Edit `lib/markdoc/doc_server.ex`:
126-
```elixir
127-
@cleanup_timeout :timer.hours(1) # 1 hour idle timeout
128-
```
129-
130-
### Change WebSocket URL
131-
132-
Edit `frontend/src/lib/PhoenixProvider.ts`:
133-
```typescript
134-
constructor(docId: string, doc: Y.Doc, wsUrl: string = "ws://localhost:4000/socket")
135-
```
136-
137-
## 🧪 Testing
138-
139-
### Manual Testing
140-
141-
1. **Basic Sync**: Open same doc in 2 tabs, type in one, see in other
142-
2. **Persistence**: Refresh a tab, content persists (loaded from history)
143-
3. **Snapshotting**: Type 50+ updates, check console for snapshot messages
144-
4. **Presence**: Open 3 tabs, verify user count and colored indicators
145-
5. **Cleanup**: Close all tabs, wait 1 hour, reopen → doc is gone
146-
147-
### Backend Unit Tests
148-
149-
```bash
150-
mix test
151-
```
152-
153-
### Backend Interactive Testing
154-
155-
```bash
156-
iex -S mix phx.server
157-
158-
# In IEx console:
159-
:observer.start() # Watch processes and memory
160-
Markdoc.DocSupervisor.count_documents() # Count active docs
161-
```
162-
163-
## 📊 Monitoring
164-
165-
### View Active Documents
166-
167-
```elixir
168-
# In IEx
169-
Markdoc.DocSupervisor.count_documents()
170-
Markdoc.DocSupervisor.list_documents()
171-
```
172-
173-
### Memory Usage
174-
175-
```elixir
176-
:observer.start() # GUI with process tree and memory stats
177-
```
178-
179-
### Logs
180-
181-
All document lifecycle events are logged:
182-
- Document spawned
183-
- User joined/left
184-
- Snapshot triggered
185-
- Document terminated
186-
187-
## 🔍 How It Works
188-
189-
### Join Sequence
190-
```
191-
Client Server GenServer
192-
| | |
193-
|--join("doc:123")------------->| |
194-
| |--start_doc("doc:123")--------->|
195-
| |<--{:ok, pid}-------------------|
196-
| |--DocServer.join(pid, self())-->|
197-
| |--DocServer.get_history(pid)--->|
198-
| |<--[blob1, blob2]---------------|
199-
|<--{:ok, %{history: [...]}}----| |
200-
| | |
201-
| Y.mergeUpdates([blob1, blob2]) |
202-
| Y.applyUpdate(doc, merged) |
203-
```
204-
205-
### Update Loop
206-
```
207-
Client A Server Client B
208-
| | |
209-
| types "a" | |
210-
| Y.js update | |
211-
|--update------->| |
212-
| |--broadcast---->|
213-
| | | Y.applyUpdate()
214-
| | | sees "a"
215-
```
216-
217-
### Snapshot Protocol
218-
```
219-
Server (after 50 updates) Client
220-
| |
221-
|--request_snapshot---------------->|
222-
| | Y.encodeStateAsUpdate(doc)
223-
|<--snapshot (compressed)-----------|
224-
| |
225-
| Replace [50 blobs] with [1 blob] |
226-
```
227-
228-
## 🚢 Production Deployment
229-
230-
### Environment Variables
231-
232-
```bash
233-
export SECRET_KEY_BASE="..." # Generate with: mix phx.gen.secret
234-
export PHX_HOST="markdoc.example.com"
235-
export PORT=4000
236-
```
237-
238-
### Build Release
239-
240-
```bash
241-
# Backend
242-
MIX_ENV=prod mix release
243-
244-
# Frontend
245-
cd frontend
246-
npm run build
247-
```
248-
249-
### Docker (Optional)
250-
251-
```dockerfile
252-
FROM elixir:1.18-alpine AS builder
253-
# ... build steps ...
254-
255-
FROM node:20-alpine AS frontend
256-
# ... frontend build ...
257-
258-
FROM alpine:3.18
259-
# ... runtime ...
101+
│ │ └── PhoenixProvider.ts # Y.js bridge
102+
│ └── components/
103+
│ └── Editor.tsx # Main editor
104+
└── docs/ # Documentation
260105
```
261106

262-
## ⚠️ Limitations
263-
264-
- **Data is NOT persistent** - Documents disappear after inactivity
265-
- **Memory-bound** - Large documents or many concurrent users consume RAM
266-
- **Single-node state** - DocServer processes live on one node (can be distributed with `global` or `Horde`)
267-
- **No authentication** - WebSocket connections are currently public
268-
- **No permissions** - All users have full edit access
269-
270-
## 🛡️ Security Considerations
271-
272-
### Recommended for Production
273-
274-
1. **Token Authentication**: Add JWT validation in `UserSocket.connect/3`
275-
2. **Rate Limiting**: Limit update frequency per connection
276-
3. **Payload Validation**: Enforce max binary size in channels
277-
4. **CORS Configuration**: Restrict allowed origins
278-
5. **HTTPS/WSS**: Use TLS in production
279-
280-
## 📖 Documentation
281-
282-
- [Architecture Design Document](docs/arch_doc.md)
283-
- [Client-Side Snapshotting Strategy](docs/client-side-snapshotting.md)
284-
285-
## 🤝 Contributing
286-
287-
This is a proof-of-concept implementation demonstrating:
288-
- Ephemeral data architecture
289-
- CRDT-based real-time sync
290-
- Actor model for concurrency
291-
- Client-side optimization strategies
292-
293-
## 📄 License
107+
## License
294108

295109
MIT License - Feel free to use and modify
296110

297-
## 🙏 Acknowledgments
111+
## Acknowledgments
298112

299-
- **Phoenix Framework**: Real-time capabilities and channels
300-
- **Y.js**: CRDT implementation
301-
- **BlockNote**: Block-based editor
302-
- **BEAM/OTP**: Supervision trees and process isolation
113+
Built with Phoenix Framework, Y.js, BlockNote, and the BEAM VM

0 commit comments

Comments
 (0)