Real-time collaborative coding for pair programming, technical interviews, and shared code execution.
PairPad is a full-stack collaborative coding platform built around a Monaco editor, Socket.IO real-time sync, and an integrated code execution engine. Multiple users can join a shared room, edit the same code simultaneously, chat, run code, and see each other's presence — all in the browser.
| Category | What It Does |
|---|---|
| Authentication | Register, login, JWT sessions, auth retry on server hiccup |
| Rooms | Create rooms with 6-character invite codes, join/leave/delete, multi-language support |
| Live Editing | Monaco Editor with full-document Socket.IO synchronization |
| Presence | Real-time list of who is online in the current room |
| Chat | Persistent in-room messaging (MongoDB-backed) with real-time broadcast |
| Code Execution | Judge0 API integration (RapidAPI or self-hosted) with automatic local fallback for JS/TS/Python |
| Security | Rate limiting on auth and execution endpoints, CORS guard, centralized error handling |
| Resilience | Infinite socket reconnection, amber pulsing "Reconnecting…" badge, transient auth-unavailable state |
- Concurrent edits use last-write-wins (CRDT/OT not yet implemented)
- Presence is tracked in-memory — not suitable for multi-server deployments without Redis
- Editor snapshots are debounced (500 ms), so a hard crash can lose the most recent keystrokes
| Layer | Technologies |
|---|---|
| Frontend | React 18, Vite, React Router v6, Axios, Socket.IO Client, Monaco Editor |
| Backend | Node.js 18+, Express 4, Socket.IO 4, MongoDB + Mongoose 8, JWT, bcryptjs, express-rate-limit |
| Code Execution | Judge0 CE (RapidAPI or self-hosted) · local Node.js / Python fallback when key is absent |
| Testing | Backend: Jest 29 + Supertest (214 tests / 17 suites, ~79% coverage) · Frontend: Vitest + Testing Library (30 tests, ~86% coverage) |
| Tooling | ESLint + Prettier (both apps) · GitHub Actions CI (lint → test → build) · coverage thresholds enforced |
| Tool | Version | Notes |
|---|---|---|
| Node.js | 18 or higher | Required |
| MongoDB | Any (local or Atlas) | Required |
| Judge0 API Key | Optional | Code execution falls back to local runner without it |
git clone https://github.com/tsunade601/pairpad.git
cd pairpadcd backend
npm install
cp .env.example .env # then fill in MONGODB_URI and JWT_SECRET
npm run devThe API starts on http://localhost:5000. Health check: GET /health.
cd ../frontend
npm install
npm run devThe app runs on http://localhost:5173. Vite proxies /api and /socket.io to the backend automatically.
Copy and edit the backend environment template:
cp backend/.env.example backend/.env| Variable | Description | Required |
|---|---|---|
PORT |
Backend port (default: 5000) |
Yes |
MONGODB_URI |
MongoDB connection string | Yes |
JWT_SECRET |
JWT signing secret (use a long random string) | Yes |
JWT_EXPIRES_IN |
Token lifetime, e.g. 7d |
No (default: 7d) |
CLIENT_URL |
Allowed browser origin for CORS and Socket.IO | Yes |
JUDGE0_BASE_URL |
Judge0 API base URL | No |
JUDGE0_API_KEY |
RapidAPI or self-hosted key | No |
JUDGE0_RAPIDAPI_HOST |
RapidAPI host header | No |
LOG_LEVEL |
Logging level: fatal/error/warn/info/debug |
No (default: debug) |
ALLOW_LOCAL_EXECUTION |
Enable the unsandboxed local runner in production (true to enable) |
No (default: disabled in prod) |
Without a Judge0 key: in development, JavaScript, TypeScript, and Python execute via the local Node.js / Python runner. Other languages require a configured Judge0 instance. In production the local runner is disabled unless
ALLOW_LOCAL_EXECUTION=true— prefer an isolated Judge0 instance.
pairpad/
├── backend/
│ ├── src/
│ │ ├── config/ # MongoDB connection
│ │ ├── controllers/ # auth, rooms, code execution, ownership transfer
│ │ ├── middleware/ # JWT auth, rate limiting, request id, error handler
│ │ ├── models/ # User, Room, Message (Mongoose)
│ │ ├── routes/ # Express route definitions
│ │ ├── services/ # Judge0 client + hardened local fallback runner
│ │ ├── sockets/ # Socket.IO collaboration handler (rate-limited, debounced)
│ │ ├── utils/ # logger, asyncHandler, validation, room access, token
│ │ └── server.js # Entry point (health/readiness probes, graceful shutdown)
│ ├── tests/ # 17 Jest test suites (214 tests, coverage thresholds)
│ ├── eslint.config.cjs # ESLint (Node + Jest)
│ └── .env.example
├── frontend/
│ ├── src/
│ │ ├── components/ # FormField, LanguageSelect, ChatPanel, ExecutionPanel
│ │ ├── constants/ # Supported languages list
│ │ ├── context/ # AuthContext (JWT + auth status machine)
│ │ ├── hooks/ # useAsyncAction, useChat, useCodeExecution, useCollaboration
│ │ ├── pages/ # Login, Register, Dashboard, Room
│ │ ├── routes/ # AppRoutes + ProtectedRoute
│ │ ├── services/ # SocketService singleton
│ │ ├── utils/ # apiError, messages (appendUniqueMessage)
│ │ └── main.jsx # Vite entry point
│ ├── index.html
│ ├── eslint.config.js # ESLint flat config (React + hooks + Vitest)
│ ├── .prettierrc.json
│ └── vite.config.js # Vite + proxy + coverage thresholds
├── .github/workflows/ci.yml # Lint → test (coverage) → build for both apps
├── docs/
│ ├── system-design.md
│ ├── DEPLOYMENT.md
├── .editorconfig
├── README.md
└── LICENSE
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/api/auth/register |
— | Create account → returns { token, refreshToken, user } |
POST |
/api/auth/login |
— | Authenticate → returns { token, refreshToken, user } |
POST |
/api/auth/refresh |
— | Refresh access token → returns { token, refreshToken } |
GET |
/api/auth/me |
Bearer | Get the current user |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/api/rooms |
Bearer | Create a room |
GET |
/api/rooms |
Bearer | List all rooms for the current user |
GET |
/api/rooms/:identifier |
Bearer | Get room by code or ID |
POST |
/api/rooms/:roomCode/join |
Bearer | Join a room |
POST |
/api/rooms/:roomCode/leave |
Bearer | Leave a room |
POST |
/api/rooms/:roomCode/transfer |
Bearer | Transfer ownership to a member (owner only) |
DELETE |
/api/rooms/:roomCode |
Bearer | Delete a room (owner only) |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/health |
— | Liveness probe (status: ok, uptime) |
GET |
/ready |
— | Readiness probe (DB connected → 200, otherwise 503) |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/api/messages/room/:roomCode |
Bearer | Chat history (last 50 messages) |
POST |
/api/execute |
Bearer | Run code via Judge0 (or local fallback) |
Execute request body:
{
"source_code": "console.log('hello')",
"language": "javascript",
"roomCode": "ABC123",
"stdin": ""
}Execute response shape:
{
"stdout": "hello\n",
"stderr": "",
"status": "success",
"time": "0.041s",
"memory": "N/A",
"exitCode": 0
}All connections require handshake.auth.token (JWT). Room membership is verified before any join-room acknowledgement.
| Event | Payload | Description |
|---|---|---|
join-room |
{ roomCode } |
Join a room channel |
leave-room |
— | Leave the current room |
code-change |
{ content, language } |
Broadcast editor content |
cursor-update |
{ position, selection } |
Share cursor position |
chat-message |
{ content } |
Send a chat message |
| Event | Payload | Description |
|---|---|---|
presence-update |
{ users[] } |
Updated online-user list |
user-joined |
{ userId, name } |
A user joined |
user-left |
{ userId, name } |
A user left |
code-change |
{ content, language, userId } |
Remote editor update |
cursor-update |
{ userId, position, selection } |
Remote cursor move |
chat-message |
{ _id, content, sender, createdAt } |
New chat message |
code-execution-result |
{ result, executedBy, language } |
Broadcast execution output |
room-deleted |
— | Room was deleted by owner |
cd backend
npm run dev # Hot-reload dev server (nodemon)
npm start # Production server
npm test # Full Jest suite with coverage (thresholds enforced)
npm run test:unit # Unit tests only (no MongoDB needed)
npm run test:watch # Watch mode
npm run lint # ESLint
npm run lint:fix # Auto-fix lint issuescd frontend
npm run dev # Vite dev server with HMR
npm run build # Production bundle → dist/
npm test # Vitest run (all tests)
npm run test:coverage# Vitest with coverage (thresholds enforced)
npm run lint # ESLint
npm run format # Prettier write
npm run preview # Preview the production buildPairPad uses a two-tier execution pipeline:
-
Judge0 API — if
JUDGE0_API_KEYis set and valid, code is submitted to Judge0 (RapidAPI or self-hosted). Supports all languages in theLANGUAGE_MAP(JS, TS, Python, Java, C, C++, Go, Rust, PHP, Ruby). -
Local fallback — if the key is absent, is the placeholder value, or if Judge0 returns an error, PairPad can execute JavaScript/TypeScript with the local
noderuntime and Python with the localpython/python3runtime. This runs in a child process with a scrubbed environment (no app secrets), a 5-second timeout, a 128 MB heap cap, and a 1 MB output cap.
⚠️ Security note: the local runner is a resource guard, not a full sandbox (no container/seccomp/cgroups). It is disabled in production unlessALLOW_LOCAL_EXECUTION=trueis set. Prefer a fully isolated runner (Judge0 / containerized) in production.
Results are returned in the HTTP response and broadcast to the entire room via code-execution-result.
- Persistent editor snapshots on the Room document (debounced persistence)
-
stdininput textarea in the Run Code panel - Room invite code shown in-header with copy-to-clipboard
- Ownership transfer (owner → member)
- CI/CD pipeline (GitHub Actions) with lint, format, test, and coverage gates
- Hardened local code execution (env scrubbing + resource limits + production gating)
- Centralized error handling with request-ids + structured logging +
/health//ready - CRDT / Operational Transform for conflict-free concurrent editing
- Remote cursor rendering in Monaco (delta decorations)
- Role-based permissions (owner / editor / viewer)
- Redis adapter for multi-instance Socket.IO horizontal scaling
- Docker Compose setup with a single
docker compose up - Interview mode — countdown timer, problem packs, hidden test cases
- Fork and create a feature branch from
main - Keep changes focused; one concern per PR
- Add or update tests when modifying API or socket behavior
- Ensure
npm testpasses in bothbackend/andfrontend/before opening a PR - Open a pull request with a clear description
MIT — see LICENSE for details.