Real-time collaborative code editor with Docker-sandboxed execution
CollabCode lets multiple developers edit code simultaneously in a shared browser-based editor — every keystroke is synced instantly across all connected clients using CRDTs (Conflict-free Replicated Data Types). Code can be run directly in the browser against language-specific Docker containers with strict resource isolation.
- Real-time collaboration — Monaco Editor (same engine as VS Code) with Yjs CRDT sync. No conflicts, no locks, no last-write-wins.
- Live cursor presence — see where every collaborator's cursor is, highlighted with their unique color and name label.
- Docker-sandboxed execution — code runs in isolated containers with CPU, memory, network, and filesystem restrictions.
- Multi-language support — Python 3.12, JavaScript (Node 20), Java 21, C++17.
- Persistent documents — room content is auto-saved to PostgreSQL every 60 seconds using gzip-compressed Yjs snapshots.
- Horizontally scalable — Redis pub/sub fans Yjs updates across multiple server instances.
- Shareable rooms — one URL to share, anyone can join and start editing immediately.
┌──────────────────────────────────────────────────────────────────────┐
│ Browser (React 18 + TypeScript) │
│ │
│ ┌─────────────────────────────┐ ┌──────────────────────────────┐ │
│ │ Monaco Editor │ │ Output Panel │ │
│ │ (@monaco-editor/react) │ │ (execution results, │ │
│ │ ▲ ▼ │ │ ANSI terminal output) │ │
│ │ MonacoBinding (y-monaco) │ └──────────────────────────────┘ │
│ │ ▲ ▼ │ │
│ │ Y.Text (Yjs CRDT) │ ┌──────────────────────────────┐ │
│ │ ▲ ▼ │ │ PresenceBar │ │
│ │ WebsocketProvider │ │ (y-protocols/awareness) │ │
│ │ (y-websocket) │ └──────────────────────────────┘ │
│ └──────────┬──────────────────┘ │
└─────────────│────────────────────────────────────────────────────────┘
│ WebSocket ws://host/yjs?room=<roomId>
▼
┌─────────────────────────────────────────────────────────────────────┐
│ CollabCode Server (Node.js + Express) │
│ │
│ ┌──────────────────────┐ ┌──────────────────────────────────┐ │
│ │ Yjs Sync Server │ │ REST API │ │
│ │ (yjs-server.ts) │ │ POST /api/execute │ │
│ │ │ │ GET/POST/PATCH /api/rooms │ │
│ │ y-protocols/sync │ │ GET /api/execute/history/:id │ │
│ │ y-protocols/aware. │ └──────────────────────────────────┘ │
│ └──────────┬───────────┘ │ │
│ │ ▼ │
│ ┌────────▼────────┐ ┌────────────────────────┐ │
│ │ Redis pub/sub │ │ Docker sandbox │ │
│ │ (fan-out to │ │ docker run --rm │ │
│ │ other nodes) │ │ --network none │ │
│ └─────────────────┘ │ --memory 128m │ │
│ │ --cpu-quota 50000 │ │
│ ┌─────────────────┐ │ --read-only │ │
│ │ PostgreSQL │ │ --cap-drop ALL │ │
│ │ - rooms │ └────────────────────────┘ │
│ │ - snapshots │ │
│ │ - exec history │ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
CollabCode uses Yjs — a high-performance CRDT library that enables conflict-free merging of concurrent edits.
Traditional collaborative editors (early Google Docs) use Operational Transformation. OT requires a central server to serialize all operations and is notoriously difficult to implement correctly — even Google spent years on it.
Yjs uses an algorithm called YATA (Yet Another Transformation Approach):
-
Every character insertion is a unique node with:
- A globally unique ID:
(clientId, clock)whereclientIdis the Yjs client ID andclockis a monotonically increasing counter - A reference to the left and right neighbors at insertion time
- A globally unique ID:
-
When two clients insert at the same position simultaneously, Yjs applies a deterministic tiebreak rule:
- The client with the higher client ID wins and its insertion goes first
- This rule is pure — it produces the same result on all peers independently
-
Result: All clients converge to the same document without a central arbiter, and without any operation history (unlike OT which requires the full operation log to transform against).
Client A types "Hello" at position 0
Client B types "World" at position 0 (simultaneously, before sync)
OT result: May differ per implementation — requires server to serialize
CRDT result: Both clients always converge to the same "HelloWorld" or "WorldHello"
(deterministically, based on client IDs)
The CRDT approach also enables offline editing — changes queue locally and are merged when connectivity is restored, with no risk of data loss.
Code execution is sandboxed using multiple complementary mechanisms:
| Mechanism | Flag | Purpose |
|---|---|---|
| No network | --network none |
Prevents outbound connections, data exfiltration |
| Read-only FS | --read-only |
Prevents persistent filesystem writes |
| Writable tmpfs | --tmpfs=/tmp:size=32m |
Allows temp files up to 32 MB |
| Memory limit | --memory=128m |
Hard RAM cap, prevents OOM attacks |
| Swap disabled | --memory-swap=128m |
Eliminates swap-based memory bypass |
| CPU quota | --cpu-quota=50000 |
50% of one CPU core (CFS scheduler) |
| Drop capabilities | --cap-drop=ALL |
Removes all Linux capabilities (no raw sockets, mount, etc.) |
| No privilege escalation | --no-new-privileges |
Prevents setuid/setgid binaries from gaining root |
| Non-root user | --user=65534:65534 |
Runs as nobody — no root inside container |
| Auto-remove | --rm |
Container destroyed immediately after execution |
| Timeout | 5 seconds | Force-kills container at deadline (docker kill) |
| Fresh container per run | Yes | No state persists between executions |
Before spawning a container, the API layer:
- Content pre-flight (
security.ts): scans for fork bombs, Docker socket access patterns, sensitive file reads - Rate limiting: 10 executions per room per minute (in-memory sliding window), plus 100 API requests per minute per IP (Express rate limiter)
- Input size cap: code must be < 100,000 characters
- Node.js 20+
- Docker Engine 24+
- Docker Compose v2+
- PostgreSQL 16+ (or Docker)
- Redis 7+ (or Docker)
# Clone the repository
git clone https://github.com/rohanmukka/CollabCode.git
cd CollabCode
# Copy environment variables
cp .env.example .env
# Edit .env to set JWT_SECRET to a strong random value
# Build execution sandbox images (one-time setup)
bash docker/build-images.sh
# Start all services
docker compose up -d
# Open the app
open http://localhost:3000# 1. Install dependencies
npm install
npm install --prefix server
# 2. Start PostgreSQL and Redis (Docker for convenience)
docker run -d --name pg -e POSTGRES_DB=collabcode -e POSTGRES_USER=collabcode \
-e POSTGRES_PASSWORD=collabcode -p 5432:5432 postgres:16-alpine
docker run -d --name redis -p 6379:6379 redis:7-alpine
# 3. Apply database schema
psql postgresql://collabcode:collabcode@localhost:5432/collabcode \
-f server/sql/init.sql
# 4. Build sandbox images
bash docker/build-images.sh
# 5. Start frontend + backend concurrently
npm run dev| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
— | PostgreSQL connection string |
REDIS_URL |
— | Redis connection string |
JWT_SECRET |
change_me… |
JWT signing secret (must be changed!) |
EXECUTION_TIMEOUT_MS |
5000 |
Sandbox execution wall-clock timeout |
EXECUTION_MEMORY_LIMIT |
128m |
Docker container memory limit |
EXECUTION_CPU_QUOTA |
50000 |
CPU quota in µs (50% of 1 core) |
PORT |
4000 |
API + WebSocket server port |
VITE_API_URL |
`` | Frontend: base URL for API calls |
VITE_WS_URL |
ws://<host> |
Frontend: WebSocket server URL |
CollabCode/
├── src/ # Frontend (React + TypeScript)
│ ├── components/
│ │ ├── Editor.tsx # Monaco + Yjs binding
│ │ ├── PresenceBar.tsx # User cursors and avatars
│ │ ├── OutputPanel.tsx # Execution results
│ │ ├── RoomHeader.tsx # Room info, language, share, run
│ │ ├── ConnectionBanner.tsx # WebSocket status banner
│ │ └── ErrorBoundary.tsx # React error boundary
│ ├── hooks/
│ │ ├── useCollaboration.ts # Yjs session lifecycle
│ │ ├── useAwareness.ts # Reactive presence state
│ │ └── useConnectionStatus.ts # WS connection tracking
│ ├── lib/
│ │ ├── collaboration.ts # Yjs provider setup
│ │ └── api.ts # REST API client
│ ├── pages/
│ │ ├── HomePage.tsx # Create / join room
│ │ └── RoomPage.tsx # Collaborative editor room
│ └── types/
│ └── index.ts # Shared TypeScript types
├── server/ # Backend (Node.js + TypeScript)
│ ├── src/
│ │ ├── index.ts # Express + WebSocket server
│ │ ├── yjs-server.ts # Yjs document sync
│ │ ├── rooms.ts # Room CRUD API
│ │ ├── execution.ts # Docker sandbox manager
│ │ ├── persistence.ts # PostgreSQL snapshots
│ │ ├── redis-pubsub.ts # Redis fan-out
│ │ └── security.ts # Auth, rate limiting, code scan
│ ├── sql/
│ │ └── init.sql # Database schema
│ ├── package.json
│ └── tsconfig.json
├── docker/ # Execution sandbox images
│ ├── Dockerfile.python # Python 3.12
│ ├── Dockerfile.node # Node.js 20
│ ├── Dockerfile.java # Java 21 (Eclipse Temurin)
│ ├── Dockerfile.cpp # GCC 13 C++17
│ └── build-images.sh # Build all images script
├── docker-compose.yml # Full stack compose config
├── Dockerfile # Production app image
├── vite.config.ts # Vite + Monaco chunking
└── tailwind.config.js # Design system tokens
- Performance: Yjs is the fastest CRDT library available (see CRDT benchmarks)
- Monaco integration:
y-monacoprovides first-class binding with zero custom code - Awareness protocol: Yjs ships cursor/presence awareness out of the box
- Binary protocol: Yjs uses a compact binary encoding — much smaller than JSON-based sync
- Feature parity with VS Code: same language servers, same IntelliSense, same keybindings
- TypeScript support: first-class TypeScript language features built in
- Ecosystem: more Monaco-specific collaboration tooling (
y-monaco,monaco-languageclient)
- Language breadth: Docker supports any language runtime without reimplementing it
- Standard tooling: same flags work for all languages (
--network none,--memory, etc.) - Observability: standard Docker metrics, logs, and process management
- Tradeoff: Docker is heavier than WASM sandboxing but simpler to maintain multi-language support
Storing Y.encodeStateAsUpdate() binary (gzip-compressed) has several advantages:
- CRDT-safe recovery: applying a snapshot is idempotent — no risk of double-applying
- No diff library needed: Yjs handles the diff/merge internally
- Merge friendly: any two snapshots can be merged via
Y.applyUpdate - Compact: typical code documents compress to 200-2000 bytes
| Layer | Technology | Version |
|---|---|---|
| Frontend framework | React | 18 |
| Language | TypeScript | 5 |
| Editor | Monaco Editor | 0.44 |
| CRDT | Yjs | 13 |
| WS provider | y-websocket | 1.5 |
| Monaco binding | y-monaco | 0.1 |
| Styling | Tailwind CSS | 3 |
| Router | React Router | 6 |
| Build tool | Vite | 5 |
| Backend | Express | 4 |
| WebSocket server | ws | 8 |
| Database | PostgreSQL | 16 |
| Cache / pub-sub | Redis | 7 |
| Container runtime | Docker | 24+ |
| Auth | JSON Web Tokens | — |
MIT — see LICENSE file.