Real-time collaborative documents — no accounts, no setup.
Share a 4-letter code. Start writing together.
jotdaddy.com · Quick start · Architecture · Deployment
JotDaddy is a real-time collaborative text editor. Open the page, share the 4-letter document code with anyone, and edit together — cursors, presence, and changes sync instantly.
- No accounts — just a code to share
- Offline-first — edits queue locally and sync when you reconnect
- CRDT-based — Yjs ensures conflict-free merges, no matter who types what
- Self-hostable — one
docker compose upand it's running
- Real-time collaborative editing (Yjs CRDT)
- Live cursor and presence indicators per collaborator
- Automatic document expiry (5 hours after last person leaves)
- Version history with named saves and restore
- Export to Markdown, plain text, DOCX, PDF
- Offline support — edits persist locally via IndexedDB, sync on reconnect
- Horizontal scaling via Redis pub/sub (Yjs updates + awareness across servers)
Browser (React + Tiptap + Yjs)
│
│ HTTPS / WSS
▼
┌─────────────────────────────────────────┐
│ Nginx (reverse proxy) │
│ ip_hash sticky sessions for WebSocket │
│ SSL termination · rate limiting │
└──────────┬──────────────────┬──────────┘
│ │
┌───────▼───────┐ ┌───────▼───────┐
│ App server 1 │ │ App server 2 │
│ Express + WS │ │ Express + WS │
│ Yjs in-memory │ │ Yjs in-memory │
└───────┬───────┘ └───────┬───────┘
│ Redis pub/sub │
│ ydoc:<docId> │
│ awareness:<docId>│
└────────┬──────────┘
│
┌───────▼───────┐
│ Redis │
│ Yjs relay │
│ Awareness │
│ Client count │
└───────┬───────┘
│
┌───────▼───────┐
│ PostgreSQL │
│ documents │
│ versions │
│ ydoc state │
└───────────────┘
- User types → Tiptap generates a Yjs update
WebsocketProvidersends it to the app server as a binary message- Server applies it to the in-memory Y.Doc and publishes to Redis
ydoc:<docId> - The other app server receives it from Redis, applies it locally, and broadcasts to its connected clients
- All clients see the change within ~50ms on the same region
Yjs Awareness tracks cursor position, name, and color per client. The same Redis relay path broadcasts awareness updates across servers so users on different containers see each other's cursors.
y-indexeddb persists the full Y.Doc in the browser's IndexedDB. On open, the editor loads instantly from local cache. While offline, edits are written to IndexedDB. On reconnect, the CRDT merge resolves everything automatically — no conflicts.
| Layer | Technology |
|---|---|
| Frontend | React 18, Tiptap, Yjs, y-websocket, y-indexeddb |
| Backend | Node.js, Express, ws |
| Realtime | y-websocket, Yjs CRDT |
| Database | PostgreSQL 16 |
| Caching / pub-sub | Redis 7 |
| Load balancer | Nginx (ip_hash for WS sticky sessions) |
| Observability | Prometheus + Grafana |
| CI/CD | GitHub Actions → ghcr.io → EC2 rolling deploy |
| Infrastructure | Docker Compose, EC2 |
git clone https://github.com/khulon/jotdaddy
cd jotdaddy
# Install dependencies
npm install
# Start Postgres and Redis via Docker
docker compose -f docker-compose.dev-lb.yml up postgres redis -d
# Copy and edit env
cp server/.env.example server/.env
# Run dev servers (frontend + backend with hot reload)
npm run dev
# → App: http://localhost:5173
# → API: http://localhost:3001/api# Full stack: Nginx + 2 app containers + Postgres + Redis + Prometheus + Grafana
docker compose -f docker-compose.dev-lb.yml up --build
# App: http://localhost:8080
# Prometheus: http://localhost:9090
# Grafana: http://localhost:3000 (admin/admin)Full instructions in DEPLOY.md and CICD.md.
Quick version:
- Provision an Ubuntu VPS (2+ vCPU, 2GB+ RAM)
- Install Docker:
curl -fsSL https://get.docker.com | sudo sh - Copy project, create
.envfrom.env.example - Get SSL cert:
certbot certonly --standalone -d yourdomain.com docker compose -f docker-compose.prod.yml up -d
CI/CD (push-to-deploy):
- Push repo to GitHub
- Add 4 secrets:
EC2_HOST,EC2_USER,EC2_SSH_KEY,EC2_DEPLOY_PATH git push origin main→ deploys in ~2 minutes with zero downtime rolling restart
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
Yes | PostgreSQL connection string |
REDIS_URL |
No | Redis URL — omit for single-server mode |
PORT |
No | Server port (default: 3001) |
CLIENT_ORIGIN |
Yes (prod) | Frontend origin for CORS |
SERVER_ID |
No | Instance label for Prometheus metrics |
MAX_WS_PER_IP |
No | Max WebSocket connections per IP (default: 15) |
MAX_WS_PER_DOC |
No | Max concurrent editors per doc (default: 50) |
MAX_WS_MSG_RATE |
No | Max WS messages/sec per connection (default: 60) |
cd loadtest
# Install k6: brew install k6
# REST API performance
k6 run -e BASE_URL=https://jotdaddy.com http-api.js
# WebSocket sync latency
k6 run -e BASE_URL=https://jotdaddy.com websocket-sync.js
# Concurrent users stress test (pre-creates doc pool, bypasses rate limiter)
k6 run -e BASE_URL=https://jotdaddy.com stress-ws.jsSee LOADTEST.md for full instructions.
jotdaddy/
├── client/ # React frontend (Vite)
│ ├── public/ # favicon.svg, og-image.png
│ └── src/
│ ├── components/ # EditorPage, LandingPage, Topbar, Sidebar...
│ ├── hooks/ # useYjsEditor, useUserIdentity
│ └── lib/ # api.js, export.js
│
├── server/ # Node.js backend
│ └── src/
│ ├── index.js # Express REST API + rate limiting
│ ├── collaboration.js # WebSocket + Yjs + Redis pub/sub
│ ├── db.js # PostgreSQL queries
│ └── metrics.js # Prometheus metrics
│
├── nginx/
│ ├── nginx.prod.conf # SSL + ip_hash + rate limits
│ └── nginx.dev.conf # Local dev (no SSL)
│
├── monitoring/
│ ├── prometheus.dev.yml # Scrape config for local dev
│ └── grafana/ # Auto-provisioned dashboard
│
├── loadtest/ # k6 load test scripts
│
├── docker-compose.prod.yml # Production (pulls from ghcr.io)
├── docker-compose.dev-lb.yml # Local with full stack
└── .github/workflows/
└── deploy.yml # CI/CD pipeline
- Nginx rate limiting: 5 new WS connections/sec per IP, max 50 concurrent connections per IP
- Node.js limits: max 15 WS connections per IP, max 50 editors per document, max 60 messages/sec per connection
- HTTPS enforced via Nginx redirect
- Prometheus
/metricsendpoint restricted to internal Docker network - No user accounts or PII stored — documents are anonymous by design
MIT