Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Repository files navigation

CollabCode

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.


Features

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

Architecture

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

CRDT Explained

CollabCode uses Yjs — a high-performance CRDT library that enables conflict-free merging of concurrent edits.

The Problem with OT (Operational Transformation)

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.

How Yjs CRDTs Work

Yjs uses an algorithm called YATA (Yet Another Transformation Approach):

  1. Every character insertion is a unique node with:

    • A globally unique ID: (clientId, clock) where clientId is the Yjs client ID and clock is a monotonically increasing counter
    • A reference to the left and right neighbors at insertion time
  2. 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
  3. 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).

Why This Matters

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.


Security Model

Docker Sandbox Layers

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

Application-Level Checks

Before spawning a container, the API layer:

  1. Content pre-flight (security.ts): scans for fork bombs, Docker socket access patterns, sensitive file reads
  2. Rate limiting: 10 executions per room per minute (in-memory sliding window), plus 100 API requests per minute per IP (Express rate limiter)
  3. Input size cap: code must be < 100,000 characters

Setup Guide

Prerequisites

  • Node.js 20+
  • Docker Engine 24+
  • Docker Compose v2+
  • PostgreSQL 16+ (or Docker)
  • Redis 7+ (or Docker)

Quick Start (Docker Compose)

# 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

Development Setup (Manual)

# 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

Environment Variables

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

Project Structure

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

Design Decisions

Why Yjs over ShareDB / Automerge?

  • Performance: Yjs is the fastest CRDT library available (see CRDT benchmarks)
  • Monaco integration: y-monaco provides 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

Why Monaco over CodeMirror 6?

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

Why Docker over gVisor / WASM sandboxing?

  • 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

Why PostgreSQL + gzip snapshots over text diffs?

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

Tech Stack

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

License

MIT — see LICENSE file.

About

Real-time collaborative code editor with CRDT-based sync (Yjs), Monaco Editor, Docker-sandboxed execution, presence indicators, and multi-language support.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages