Real-time collaborative code editor — register, create a room with a shared password, and start coding together instantly in the browser.
- Overview
- Tech Stack
- Project Structure
- Getting Started
- REST API Endpoints
- WebSocket Events
- Application Workflow
- Load Testing Results
CodeLink is a full-stack collaborative coding platform where multiple users can edit the same code in real time. Authentication is JWT-based; rooms are protected by a shared password. Live code synchronisation is handled over WebSocket (STOMP over SockJS), while room metadata is persisted in PostgreSQL and the active code buffer is cached in Redis for sub-millisecond reads.
| Feature | Details |
|---|---|
| 🔐 Authentication | JWT-secured register / login — no email required |
| 🚪 Room creation | Any authenticated user creates a password-protected room |
| ⚡ Live code sync | STOMP over SockJS; edits broadcast to all subscribers in < 100 ms |
| 💾 Dual-layer persistence | Redis (live buffer) + PostgreSQL (durable store) |
| 🧹 Viewer presence | Real-time viewer count pushed on connect / disconnect |
| 🗂 Multi-language editor | Monaco Editor with syntax highlighting |
| 🌓 Dark / light theme | Persistent theme preference |
| Layer | Technology |
|---|---|
| Runtime | Java 21 |
| Framework | Spring Boot 3.4.2 |
| Security | Spring Security + JJWT 0.12.6 (JWT) |
| Real-time | Spring WebSocket (STOMP broker) |
| ORM | Spring Data JPA / Hibernate |
| Database | PostgreSQL 14+ |
| Cache | Redis (Upstash, SSL) via Spring Data Redis |
| Build | Maven 3.9+ / Maven Wrapper |
| Container | Docker (multi-stage Dockerfile) |
| Layer | Technology |
|---|---|
| Framework | React 19 + Vite 8 |
| Routing | React Router DOM v7 |
| Editor | Monaco Editor (@monaco-editor/react) |
| WebSocket | STOMP.js + SockJS-client |
| Icons | Lucide React |
| Styling | Tailwind CSS v4 |
| Linting | OxLint |
CodeLink/
├── CodeLink-backend/
│ ├── src/main/java/org/code/codelink/
│ │ ├── config/ # CORS + WebSocket / STOMP broker config
│ │ ├── controller/
│ │ │ ├── AuthController.java # POST /api/auth/register, /login
│ │ │ └── RoomController.java # CRUD /api/rooms
│ │ ├── dto/ # Request / Response DTOs
│ │ ├── exception/ # GlobalExceptionHandler
│ │ ├── model/ # JPA entities: User, Room
│ │ ├── repository/ # Spring Data repos: UserRepository, RoomRepository
│ │ ├── security/ # JwtService, JwtFilter, SecurityConfig
│ │ ├── service/ # (future services)
│ │ └── websocket/
│ │ ├── RoomWebSocketController.java # STOMP message handlers
│ │ ├── WsChannelInterceptor.java # JWT auth on WS connect
│ │ └── WsMessages.java # Payload record types
│ └── src/main/resources/
│ └── application.properties
│
├── CodeLink-frontend/
│ └── src/
│ ├── components/
│ │ ├── EditorHeader.jsx # Toolbar: language, viewers, theme toggle
│ │ ├── IconRail.jsx # Sidebar icon navigation
│ │ └── ui/ # Reusable UI primitives
│ ├── contexts/
│ │ └── AuthContext.jsx # JWT token storage + getToken() helper
│ ├── pages/
│ │ ├── LoginPage.jsx # Login form
│ │ ├── RegisterPage.jsx # Registration form
│ │ ├── CreateRoomPage.jsx # Room creation with optional password
│ │ └── EditorPage.jsx # Live editor + viewer count
│ └── services/
│ ├── authApi.js # register / login HTTP calls
│ ├── roomApi.js # createRoom / joinRoom / getRoom / deleteRoom
│ └── socketService.js # STOMP connect / publish / disconnect
│
├── docs/
│ └── load-test-results.png # Load testing dashboard screenshot
└── vercel.json # Frontend deployment config
| Tool | Version |
|---|---|
| Java | 21+ |
| Maven | 3.9+ (or use included mvnw) |
| Node.js | 18+ |
| PostgreSQL | 14+ |
| Redis | Any (Upstash cloud or local) |
-- Run as a PostgreSQL superuser:
CREATE DATABASE codelink;Hibernate auto-creates all tables on first boot (ddl-auto: update).
# Copy and fill in your secrets
cp CodeLink-backend/.env.example CodeLink-backend/.env# CodeLink-backend/.env
SERVER_PORT=8081
REDIS_HOST=valued-mammal-99002.upstash.io
REDIS_PORT=6379
REDIS_PASSWORD=your_redis_password
REDIS_SSL_ENABLED=true
CORS_ALLOWED_ORIGINS=http://localhost:5173
ROOM_EXPIRY_HOURS=24
JWT_SECRET=your_base64_encoded_256bit_secret# Windows
cd CodeLink-backend && mvnw.cmd spring-boot:run
# macOS / Linux
cd CodeLink-backend && ./mvnw spring-boot:runBackend starts on http://localhost:8081.
cd CodeLink-frontend
cp .env.example .env # pre-filled for local dev
npm install
npm run devFrontend starts on http://localhost:5173.
# CodeLink-frontend/.env
VITE_API_BASE_URL=http://localhost:8081cd CodeLink-backend
docker build -t codelink-backend .
docker run -p 8081:8081 --env-file .env codelink-backendBase URL: http://localhost:8081/api
All protected routes require the header:
Authorization: Bearer <jwt_token>
| Method | Path | Auth | Request Body | Response | Description |
|---|---|---|---|---|---|
POST |
/api/auth/register |
Public | { "username": "alice", "password": "secret123" } |
{ "token": "...", "username": "alice" } |
Register a new user; returns a JWT |
POST |
/api/auth/login |
Public | { "username": "alice", "password": "secret123" } |
{ "token": "...", "username": "alice" } |
Authenticate; returns a JWT |
| Method | Path | Auth | Request Body | Response | Description |
|---|---|---|---|---|---|
POST |
/api/rooms |
Required | { "password": "mypass" } |
RoomResponse |
Create a new room; password is optional |
POST |
/api/rooms/join |
Public | { "password": "mypass" } |
RoomResponse |
Join an existing room by its shared password |
GET |
/api/rooms/{roomId} |
Public | — | RoomResponse |
Fetch room metadata by ID |
DELETE |
/api/rooms/{roomId} |
Required (owner) | — | 204 No Content |
Delete a room; only the owner may delete |
RoomResponse shape:
{
"roomId": "a3f9c1d820",
"owner": "alice",
"createdAt": "2026-08-04T15:25:10Z"
}The POST /api/rooms/join endpoint uses a two-layer lookup strategy to avoid slow BCrypt verification on every join:
Client sends password
│
▼
SHA-256(password) → Redis hash lookup (< 1 ms)
│
Hit? ──Yes──► Return room from cache (no BCrypt)
│
No
│
▼
DB lookup by SHA column (O(1) index scan)
│
BCrypt.matches() verify
│
▼
Re-populate Redis cache → Return RoomResponse
Endpoint: ws://localhost:8081/ws (SockJS fallback enabled)
Connect with STOMP headers:
Authorization: Bearer <jwt_token>
roomId: <roomId>
Subscribe to receive all room events:
/topic/room/{roomId}
| Destination | Body | Description |
|---|---|---|
/app/room/{roomId}/sync |
{} |
Request current code state on first load |
/app/room/{roomId}/edit |
{ "code": "...", "senderSession": "abc123" } |
Broadcast a code edit to all subscribers |
All messages arrive on /topic/room/{roomId}.
CodeBroadcast — sent on sync and every edit:
{
"type": "CODE",
"code": "console.log('hello')",
"viewers": 3,
"senderSession": "abc123"
}ViewerCount — sent on every connect / disconnect:
{
"type": "VIEWERS",
"viewers": 2
}
senderSessionlets the originating tab suppress the echo — only other tabs apply the incoming edit.
┌─────────────────────────────────────────────────────────────┐
│ BROWSER (Client) │
│ │
│ 1. Register / Login ──HTTP POST /api/auth──► JWT Token │
│ │
│ 2. Create Room ──HTTP POST /api/rooms──► { roomId } │
│ OR │
│ Join Room ──HTTP POST /api/rooms/join──► { roomId } │
│ │
│ 3. Open EditorPage │
│ ├─ Connect STOMP (SockJS) with JWT + roomId header │
│ ├─ Server increments viewer count → broadcast VIEWERS │
│ ├─ Publish SYNC → receive current code from Redis/DB │
│ └─ Monaco Editor ready │
│ │
│ 4. Type in editor │
│ └─ Publish EDIT { code, senderSession } │
│ │ │
│ ▼ │
│ Server writes to Redis (sync) + PostgreSQL (async) │
│ │ │
│ ▼ │
│ Broadcast CODE to all /topic/room/{roomId} subscribers │
│ │ │
│ Other tabs receive → apply if senderSession ≠ self │
│ │
│ 5. Close tab / navigate away │
│ └─ STOMP disconnect → viewer count decremented │
│ │
└─────────────────────────────────────────────────────────────┘
┌──────────────┐ JWT Auth ┌──────────────────────┐
│ Frontend │◄───────────────────►│ Spring Boot API │
│ React/Vite │ REST + STOMP │ :8081 │
└──────────────┘ └──────┬───────────────┘
│
┌────────────┴────────────┐
│ │
┌──────▼──────┐ ┌───────▼──────┐
│ PostgreSQL │ │ Redis │
│ (durable │ │ (live buffer │
│ store) │ │ + room cache)│
└─────────────┘ └──────────────┘
1. Register/Login
└── POST /api/auth/register or /login
└── Receive JWT token → stored in AuthContext (sessionStorage)
2. Create a Room
└── POST /api/rooms { password: "mypass" }
├── Room saved to PostgreSQL (owner = authenticated user)
├── SHA-256(password) stored for fast O(1) DB lookups
├── Room metadata cached in Redis Hash (TTL: 24h)
└── Receive { roomId, owner, createdAt }
3. Share Room
└── Share the password with collaborators (out-of-band)
4. Collaborator Joins
└── POST /api/rooms/join { password: "mypass" }
├── SHA-256 checked in Redis → cache hit? Return immediately (< 1 ms)
└── Cache miss? DB lookup by SHA → BCrypt verify → re-cache
5. Enter Editor (both users)
└── STOMP connect to ws://localhost:8081/ws
├── WsChannelInterceptor validates JWT on connect
├── Server maps STOMP sessionId → roomId in-memory
├── Viewer count incremented → ViewerCount broadcast
├── Client publishes SYNC → server sends current code from Redis
└── Monaco Editor displays synced code
6. Collaborative Editing
└── User A types → EDIT published → Redis updated → CODE broadcast
└── User B receives CODE → senderSession ≠ B's session → apply diff
7. Leave
└── STOMP disconnect event → viewer count decremented → broadcast
Load test run against the deployed backend using 25 Virtual Users for 1 minute (Fixed profile).
| Metric | Value |
|---|---|
| Total Requests Sent | 3,300 |
| Requests / second | 82.86 |
| Avg. Response Time | 262 ms |
| P90 | 433 ms |
| P95 | 738 ms |
| P99 | 1,473 ms |
| Error % | 0.00% ✅ |
| Failure % | 0.00% ✅ |
| Peak CPU % | 40.9% |
| Peak Memory % | 96.7% |
- Zero errors or failures across all 3,300 requests under 25 concurrent users.
- Avg. response time of 262 ms is well within acceptable range for a WebSocket-heavy collaborative app.
- The Redis caching layer (room-join bypass of BCrypt) is the primary reason P50/P90 stays low — only P99 (1,473 ms) shows occasional DB/BCrypt overhead on cache-cold joins.
- Peak Memory at 96.7% indicates the process is memory-bound at this concurrency level; scaling horizontally or increasing heap is recommended for > 50 VUs.
- CPU at 40.9% leaves comfortable headroom for additional load.
| Variable | Default | Description |
|---|---|---|
SERVER_PORT |
8081 |
HTTP port the app listens on |
REDIS_HOST |
— | Redis hostname |
REDIS_PORT |
6379 |
Redis port |
REDIS_PASSWORD |
— | Redis auth password |
REDIS_SSL_ENABLED |
true |
Enable TLS for Redis connection |
CORS_ALLOWED_ORIGINS |
http://localhost:5173 |
Comma-separated allowed origins |
ROOM_EXPIRY_HOURS |
24 |
Redis TTL for room code buffer |
JWT_SECRET |
— | Base64-encoded 256-bit HMAC secret |
| Variable | Default | Description |
|---|---|---|
VITE_API_BASE_URL |
http://localhost:8081 |
Backend base URL |
The repository is pre-configured for Vercel (frontend) via vercel.json. The backend can be containerised using the provided Dockerfile.
# Build backend JAR
cd CodeLink-backend && mvnw.cmd package -DskipTests
# Build Docker image
docker build -t codelink-backend .Set all environment variables in your cloud provider's dashboard before deploying.
MIT
