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
5044mix deps.get
5145mix compile
5246```
5347
54- ** Frontend :**
48+ 2 . ** Install frontend dependencies :**
5549``` bash
5650cd frontend
5751npm install
5852cd ..
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
7167npm 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```
8490markdoc/
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
295109MIT 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