Skip to content

Khulon/SyncDoc

Repository files navigation

JotDaddy logo

JotDaddy

Real-time collaborative documents — no accounts, no setup.

Share a 4-letter code. Start writing together.


jotdaddy.com · Quick start · Architecture · Deployment

What it is

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 up and it's running

Features

  • 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)

Architecture

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    │
              └───────────────┘

How real-time sync works

  1. User types → Tiptap generates a Yjs update
  2. WebsocketProvider sends it to the app server as a binary message
  3. Server applies it to the in-memory Y.Doc and publishes to Redis ydoc:<docId>
  4. The other app server receives it from Redis, applies it locally, and broadcasts to its connected clients
  5. All clients see the change within ~50ms on the same region

How presence works

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.

Offline support

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.


Tech stack

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

Quick start

Local development (single server)

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

Local with load balancer + monitoring

# 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)

Deployment

Full instructions in DEPLOY.md and CICD.md.

Quick version:

  1. Provision an Ubuntu VPS (2+ vCPU, 2GB+ RAM)
  2. Install Docker: curl -fsSL https://get.docker.com | sudo sh
  3. Copy project, create .env from .env.example
  4. Get SSL cert: certbot certonly --standalone -d yourdomain.com
  5. docker compose -f docker-compose.prod.yml up -d

CI/CD (push-to-deploy):

  1. Push repo to GitHub
  2. Add 4 secrets: EC2_HOST, EC2_USER, EC2_SSH_KEY, EC2_DEPLOY_PATH
  3. git push origin main → deploys in ~2 minutes with zero downtime rolling restart

Environment variables

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)

Load testing

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.js

See LOADTEST.md for full instructions.


Project structure

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

Security

  • 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 /metrics endpoint restricted to internal Docker network
  • No user accounts or PII stored — documents are anonymous by design

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors