A real-time collaborative text editor built with Rust and React. This project started as a learning exercise to master async Rust, WebSocket communication, concurrent state management, and service architecture patterns.
Multiple users can connect to the same document and edit it together in real-time. Changes from one user are instantly broadcast to all other connected users via WebSocket. The backend manages document state, handles concurrent edits, and provides a REST API for document management.
The backend is built with modern async Rust, focusing on high concurrency and performance:
- Tokio - The async runtime that powers everything. Handles thousands of concurrent connections efficiently.
- Axum - A modern, type-safe web framework built on top of Tower. Used for HTTP endpoints and WebSocket handling.
- Tower - Middleware ecosystem for request/response processing (logging, CORS, compression).
- DashMap - A concurrent HashMap that's faster than
RwLock<HashMap>because it shards data internally, allowing parallel access to different keys. - parking_lot - A faster alternative to std's RwLock with better performance for uncontended locks.
- tokio-util - Provides
CancellationTokenfor graceful shutdown of services. - async-trait - Enables async functions in traits (needed for the Service trait pattern).
- serde / serde_json - Serialization framework for JSON message handling.
- uuid - Generates unique IDs for documents and clients.
- tracing / tracing-subscriber - Structured logging for observability.
- anyhow / thiserror - Error handling libraries.
A clean React application that connects to the backend:
- React 19 - The UI framework.
- TypeScript - Type safety for the frontend code.
- Vite - Fast build tool and dev server.
- WebSocket API - Native browser WebSocket for real-time communication.
This project evolved through several versions, each introducing new concepts:
Started with a simple TCP echo server to learn Tokio fundamentals - async/await, task spawning, and Arc for shared ownership.
Added Axum web framework and WebSocket support. Implemented broadcast channels so messages from one client reach all others. Learned about socket splitting (separating read/write halves) and using tokio::select! to handle concurrent operations.
Introduced multi-document support with concurrent access. Used DashMap for storing documents (better than RwLock for concurrent access). Implemented interior mutability pattern with RwLock for document content. Added REST API endpoints for document CRUD operations. Learned about avoiding deadlocks and safely handling locks in async code.
Refactored to a service-based architecture with lifecycle management. Created a Service trait that all services implement, allowing uniform management. Built a ServiceMonitor that coordinates starting and stopping services. Implemented graceful shutdown using CancellationToken - when you press Ctrl+C, all services finish their current work cleanly before shutting down.
Three services run concurrently:
- WebSocketService - Runs the Axum HTTP/WebSocket server
- AutoSaveService - Periodically saves documents every 30 seconds
- PresenceService - Tracks which users are active in each document
# Build and run
cargo run
# The server starts on http://127.0.0.1:8080The server provides:
- REST API at
/documentsfor document management - WebSocket endpoint at
/documents/{id}/wsfor real-time editing - Health check at
/health
cd frontend
npm install
npm run devThe frontend will be available at http://localhost:5173 (or similar, Vite will tell you).
You can test the WebSocket connection using websocat:
# Install websocat
cargo install websocat
# Connect to a document
websocat ws://127.0.0.1:8080/documents/{document-id}/wsOr use the React frontend to create documents and edit them in real-time.
GET /health- Health checkGET /documents- List all documentsPOST /documents- Create a new document (body:{"title": "My Document"})GET /documents/{id}- Get document contentDELETE /documents/{id}- Delete a documentWS /documents/{id}/ws- WebSocket connection for real-time editing
Client → Server:
{"type": "Insert", "position": 0, "text": "Hello"}
{"type": "Delete", "position": 5, "length": 3}
{"type": "Sync"}Server → Client:
{"type": "Sync", "content": "Hello world", "version": 1}
{"type": "Operation", "op_type": "Insert", "position": 0, "text": "Hi", "client_id": "...", "version": 2}collab-editor/
├── src/
│ ├── main.rs # Application entry point, service orchestration
│ ├── monitor.rs # ServiceMonitor for lifecycle management
│ ├── handlers/
│ │ ├── http.rs # REST API handlers
│ │ └── websocket.rs # WebSocket handlers
│ ├── models/
│ │ ├── document.rs # Document data structure
│ │ └── operation.rs # Edit operation types
│ ├── services/
│ │ ├── service_trait.rs # Service trait definition
│ │ ├── websocket.rs # WebSocket service
│ │ ├── autosave.rs # Auto-save background service
│ │ └── presence.rs # User presence tracking service
│ └── state/
│ └── document_store.rs # Document storage with DashMap
├── frontend/
│ └── src/
│ ├── App.tsx # Main React component
│ ├── components/ # React components (Editor, DocumentList, etc.)
│ ├── hooks/
│ │ └── useWebSocket.ts # WebSocket connection hook
│ └── api.ts # REST API client
└── docs/ # Detailed documentation for each version
This project was a deep dive into:
-
Async Rust - Understanding how Tokio schedules tasks, when to use
.awaitvsspawn, and how to share state safely across async tasks. -
Web Frameworks - Axum's extractor pattern provides type-safe dependency injection. No runtime errors from missing parameters - the compiler catches them.
-
Concurrency - The difference between Mutex and RwLock, when to use DashMap, how to avoid deadlocks, and why you shouldn't hold locks across
.awaitpoints. -
Service Architecture - Building a microservice-style system with proper lifecycle management. Services can be started, monitored, and shut down gracefully.
-
Real-Time Systems - WebSocket protocol, broadcast patterns, and handling concurrent edits without conflicts.