Real-time Collaborative Architecture Whiteboard β the fastest way for developers to sketch, share, and iterate on system designs together.
- Overview
- Features
- Tech Stack
- Architecture & Flow
- Project Structure
- Getting Started
- Environment Variables
- API Reference
- WebSocket Protocol
- Permission Model
- Frontend
- Testing
- Development Workflow
- Contributing
- License
- Contact
SystemSketch solves a real friction point in remote technical collaboration: existing tools like Miro and LucidChart are too heavy for a quick 5-minute system design sketch.
SystemSketch gives developers a zero-overhead, URL-shareable whiteboard where every mouse stroke is synchronized to all participants in real time. Create a room, share the link, and start drawing β no sign-up required.
| Feature | Description |
|---|---|
| Real-time Collaboration | Multiple users draw simultaneously on the same canvas |
| WebSocket Sync | Sub-100ms updates broadcast to all connected clients in a room |
| Ghost Cursors | Live color-coded cursors show exactly where teammates are pointing |
| Optional Auth | JWT-based authentication for room ownership; anonymous access supported for public rooms |
| Room Persistence | Save and restore canvas state to/from PostgreSQL |
| Undo / Redo | Full per-client history stack with server-side broadcast |
| Granular Permissions | VIEWER β EDITOR β OWNER access control per room |
| Public / Private Rooms | Toggle room visibility; private rooms require explicit invitation |
| Export | Download the canvas as a PNG for documentation |
| Paginated Room Lists | Browse public rooms or your own rooms with offset-based pagination |
| Technology | Role |
|---|---|
| FastAPI 0.110+ | Async REST + WebSocket API framework |
| SQLAlchemy 2.0 (async) | ORM with asyncpg for non-blocking DB calls |
| Alembic | Schema migration management |
| Pydantic v2 | Request / response validation and serialization |
| python-jose / passlib | JWT signing and bcrypt password hashing |
| PostgreSQL 16 | Persistent storage for users, rooms, permissions |
| In-memory Python dict | Active canvas state cache (Redis-ready drop-in) |
| Technology | Role |
|---|---|
| Vue.js 3 (Composition API) | Reactive UI framework |
| Pinia | Centralized state management |
| Vue Router 4 | SPA routing |
| HTML5 Canvas API | High-performance drawing engine |
| Native WebSocket | Real-time bidirectional communication |
| Vite | Build tooling and dev server |
| Technology | Role |
|---|---|
| Docker & Docker Compose | Containerized local development |
| PostgreSQL 16 (Alpine) | Database container |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CLIENT (Vue.js 3) β
β β
β onMouseDown / onMouseMove / onMouseUp β
β β β² β
β β Local render β Incoming WS message β
β βΌ β β
β Canvas Engine βββββ shapes[] ββββ€ β
β β β β
β β WebSocket send β shapes[] updated β
ββββββββββΌββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββ-ββ
β β
βΌ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β BACKEND (FastAPI) β
β β
β WS /ws/{room_id} βββΊ ConnectionManager.broadcast() β
β β β
β βββββββΌβββββββ β
β β In-Memory β βββ fast read/write β
β β StateStore β for active rooms β
β βββββββ¬βββββββ β
β β (on /save) β
β βββββββΌβββββββ β
β β PostgreSQL β βββ persisted canvas state β
β ββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- User A draws a rectangle β mouse events trigger a local canvas render and a WebSocket
drawmessage. - FastAPI receives the message, updates the in-memory
StateStore, and broadcasts to all other connections in the same room. - User B receives the broadcast, pushes the shape into their local
shapes[]array, and re-renders the canvas. - When User A clicks Save,
PUT /rooms/{room_id}/savepersists the currentStateStoresnapshot to PostgreSQL. - A new joiner receives a
sync_statemessage with the full existing canvas so they are immediately up to date.
SystemSketch/
βββ docker-compose.yml # PostgreSQL service definition
βββ README.md
βββ systemsketch.md # Product specification
βββ frontendui.md # UI/UX design specification
β
βββ backend/
β βββ requirements.txt
β βββ alembic.ini
β βββ alembic/
β β βββ versions/ # Database migration scripts
β βββ app/
β βββ main.py # FastAPI app factory, router mounts
β βββ config.py # Pydantic settings (env vars)
β βββ api/
β β βββ dependencies.py # Auth dependency injection
β β βββ routes/
β β βββ auth.py # /api/v1/auth/*
β β βββ rooms.py # /api/v1/* (rooms)
β β βββ permissions.py # /api/v1/permissions/*
β β βββ websocket.py # /ws/{room_id}
β βββ core/
β β βββ database.py # Async SQLAlchemy engine + session
β β βββ state_manager.py # In-memory canvas state store
β β βββ websocket_manager.py # Connection pool + broadcast
β βββ models/
β β βββ user.py # User ORM model
β β βββ room.py # Room ORM model
β β βββ permission.py # RoomPermission ORM model + PermissionLevel enum
β βββ schemas/
β β βββ user.py # UserCreate, UserResponse, Token
β β βββ room.py # RoomCreate, RoomResponse, RoomState
β β βββ permission.py # PermissionInvite, PermissionResponse
β β βββ shape.py # Shape union type
β β βββ websocket.py # WS message schemas
β βββ services/
β βββ auth_service.py # Password hashing, JWT helpers
β βββ permission_service.py # Permission CRUD + guard logic
β
βββ frontend/
β βββ vite.config.ts
β βββ src/
β β βββ main.ts
β β βββ App.vue
β β βββ router/index.ts
β β βββ stores/
β β β βββ auth.ts # Pinia auth store
β β β βββ canvas.ts # Pinia canvas + undo/redo store
β β β βββ room.ts # Pinia room list store
β β βββ services/
β β β βββ api.ts # Axios REST client
β β β βββ websocket.ts # WS client wrapper
β β βββ views/
β β β βββ LoginView.vue
β β β βββ RegisterView.vue
β β β βββ RoomsView.vue # Room browser
β β β βββ WorkspaceView.vue # Canvas workspace
β β βββ types/index.ts # Shared TypeScript types
β βββ test/ # Vitest unit tests
β
βββ tests/ # Backend pytest suite
| Tool | Version | Install |
|---|---|---|
| Python | 3.11+ | python.org |
| Node.js | 18+ | nodejs.org |
| Docker & Docker Compose | any recent | docs.docker.com |
| Git | any | git-scm.com |
git clone https://github.com/Atulmishra22/SystemSketch.git
cd SystemSketchdocker-compose up -d postgrescd backend
# Create and activate virtual environment
python -m venv .venv
# Linux / macOS
source .venv/bin/activate
# Windows (PowerShell)
.\.venv\Scripts\Activate.ps1
# Install dependencies
pip install -r requirements.txt
# Configure environment variables
copy .env.example .env # Windows
cp .env.example .env # Linux / macOS
# Edit .env β see Environment Variables section below
# Run database migrations
alembic upgrade head
# Start the development server
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000cd frontend
# Install dependencies
npm install
# Start the Vite dev server
npm run dev| Service | URL |
|---|---|
| Frontend | http://localhost:5173 |
| Backend API | http://localhost:8000 |
| Swagger UI | http://localhost:8000/docs |
| ReDoc | http://localhost:8000/redoc |
| Health check | http://localhost:8000/health |
Full Docker Compose orchestration (backend + frontend + DB) is planned for a future release. Currently only the PostgreSQL service is containerized.
# Start only the database
docker-compose up -d postgres
# Tear down
docker-compose down
# Tear down and remove volumes (wipes DB data)
docker-compose down -vCreate a .env file inside the backend/ directory. All variables are read by app/config.py via Pydantic Settings.
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
(required) | Async PostgreSQL DSN, e.g. postgresql+asyncpg://user:pass@localhost:5432/systemsketch |
SECRET_KEY |
(required) | 256-bit random string used to sign JWTs |
ACCESS_TOKEN_EXPIRE_MINUTES |
30 |
Lifetime of access tokens in minutes |
REFRESH_TOKEN_EXPIRE_DAYS |
7 |
Lifetime of refresh tokens in days |
ALGORITHM |
HS256 |
JWT signing algorithm |
CORS_ORIGINS |
["http://localhost:5173"] |
Allowed CORS origins (JSON array) |
Example .env
DATABASE_URL=postgresql+asyncpg://systemsketch:systemsketch@localhost:5432/systemsketch
SECRET_KEY=change-me-to-a-long-random-string
ACCESS_TOKEN_EXPIRE_MINUTES=30
REFRESH_TOKEN_EXPIRE_DAYS=7
ALGORITHM=HS256
CORS_ORIGINS=["http://localhost:5173"]Full interactive docs available at http://localhost:8000/docs when the server is running.
All REST endpoints are prefixed with
/api/v1. Authentication usesAuthorization: Bearer <access_token>headers unless noted otherwise.
Register a new user account. Returns a token pair immediately.
Request body
{
"username": "alice",
"email": "alice@example.com",
"password": "securepassword"
}Response 201 Created β Token
{
"access_token": "<jwt>",
"refresh_token": "<jwt>",
"token_type": "bearer",
"user": {
"id": "uuid",
"username": "alice",
"email": "alice@example.com",
"created_at": "2026-03-01T10:00:00Z",
"last_login": "2026-03-01T10:00:00Z"
}
}Errors: 400 username or email already taken.
Authenticate with username or email and password.
Request body
{
"username": "alice",
"password": "securepassword"
}Response 200 OK β Token (same shape as register)
Errors: 401 invalid credentials.
Get the currently authenticated user's profile.
Headers: Authorization: Bearer <access_token> (required)
Response 200 OK β UserResponse
{
"id": "uuid",
"username": "alice",
"email": "alice@example.com",
"created_at": "2026-03-01T10:00:00Z",
"last_login": "2026-03-03T08:30:00Z"
}Errors: 401 missing or invalid token.
Exchange a valid refresh token for a fresh access + refresh token pair (token rotation).
Request body
{
"refresh_token": "<jwt>"
}Response 200 OK β Token (same shape as register)
Errors: 401 invalid or expired refresh token.
Create a new collaborative room.
- If authenticated: room is owned by the caller.
- If anonymous: room is created without an owner.
Request body
{
"name": "My System Design",
"is_public": true
}Response 201 Created β RoomResponse
{
"id": "uuid",
"name": "My System Design",
"is_saved": false,
"is_public": true,
"created_at": "2026-03-03T09:00:00Z",
"last_activity": "2026-03-03T09:00:00Z",
"creator_id": "uuid-or-null",
"permission_level": "public"
}List public rooms, sorted by most recent activity. Supports pagination.
Query params: limit (default 10), offset (default 0)
Response 200 OK β List[RoomResponse]
Get room metadata and the current canvas state.
- Public rooms are accessible without authentication.
- Private rooms require
VIEWERpermission or higher.
Response 200 OK β RoomState
{
"id": "uuid",
"name": "My System Design",
"shapes": [
{ "type": "rect", "x": 100, "y": 150, "width": 120, "height": 60, "color": "#2D5BFF" }
]
}Errors: 404 room not found, 403 access denied.
Persist the current in-memory canvas state to PostgreSQL. Requires EDITOR or OWNER permission.
Request body
{
"shapes": [ /* array of shape objects */ ]
}Response 200 OK β RoomResponse
Errors: 403 insufficient permission, 404 room not found.
Rename a room. Requires EDITOR or OWNER permission.
Request body
{
"name": "New Room Name"
}Response 200 OK β RoomResponse
Toggle a room between public and private. Requires OWNER permission.
Request body
{
"is_public": false
}Response 200 OK β RoomResponse
Permanently delete a room and its canvas state. Requires OWNER permission and authentication.
Response 204 No Content
Errors: 403 not the owner, 404 room not found.
List all rooms accessible by the authenticated user (owned + explicitly shared). Supports pagination.
Headers: Authorization: Bearer <access_token> (required)
Query params: limit (default 50), offset (default 0)
Response 200 OK β List[RoomWithPermission]
[
{
"id": "uuid",
"name": "My System Design",
"is_saved": true,
"is_public": false,
"created_at": "...",
"last_activity": "...",
"creator_id": "uuid",
"permission_level": "owner",
"is_owner": true,
"user_permission": "OWNER"
}
]All permission endpoints require authentication via Authorization: Bearer <access_token>.
Invite a user to a room by their username or email. Only OWNER can invite.
Request body
{
"username_or_email": "bob",
"permission": "EDITOR"
}Response 201 Created β RoomPermissionDetail
{
"id": "uuid",
"user_id": "uuid",
"room_id": "uuid",
"permission": "EDITOR",
"user": { "username": "bob", "email": "bob@example.com" }
}Errors: 404 target user not found, 403 caller is not the owner.
List all users with access to a room. Only OWNER can list permissions.
Response 200 OK β List[RoomPermissionDetail]
Update an existing user's permission level. Only OWNER can update.
Request body
{
"permission": "VIEWER"
}Response 200 OK β PermissionResponse
Revoke a user's access to a room. Only OWNER can revoke.
Response 204 No Content
Errors: 404 permission not found.
Check the calling user's own permission level for a room.
Response 200 OK β PermissionCheck
{
"has_access": true,
"permission": "EDITOR",
"is_owner": false
}Get a specific user's permission for a room. Accessible by the user themselves or the room owner.
Response 200 OK β PermissionResponse
URL: ws://localhost:8000/ws/{room_id}
Optional query parameters:
| Parameter | Description |
|---|---|
token |
JWT access token for authenticated sessions |
username |
Display name for anonymous sessions (fallback: Anonymous) |
Example
ws://localhost:8000/ws/my-room-uuid?token=<jwt>
ws://localhost:8000/ws/my-room-uuid?username=bob
All messages are JSON objects with an action discriminator field.
| Action | Description | Requires canEdit |
|---|---|---|
draw |
Add a shape to the canvas | β Yes |
cursor |
Broadcast cursor position | No |
clear |
Wipe the entire canvas | β Yes |
undo |
Undo last shape | β Yes |
redo |
Redo last undone shape | β Yes |
draw
{
"action": "draw",
"shape": {
"type": "rect",
"x": 100,
"y": 150,
"width": 120,
"height": 60,
"color": "#2D5BFF"
}
}cursor
{
"action": "cursor",
"userId": "client-uuid",
"username": "alice",
"color": "#FF5733",
"x": 342.5,
"y": 210.0
}clear
{ "action": "clear" }undo / redo
{ "action": "undo" }
{ "action": "redo" }| Action | When | Description |
|---|---|---|
sync_state |
On connect | Full canvas snapshot for the joining client |
room_users |
On connect | List of all currently connected users |
user_joined |
When someone joins | Notifies all existing users of the new participant |
user_left |
When someone disconnects | Notifies remaining users |
draw |
When someone draws | Broadcasts the new shape to all other clients |
cursor |
On cursor move | Broadcasts cursor position to all other clients |
clear |
When canvas is cleared | Notifies all clients |
undo / redo |
On undo/redo | Updated shapes array broadcast |
error |
On invalid action | Error message with optional code |
sync_state (sent to newly joined client)
{
"action": "sync_state",
"shapes": [ /* full array of current shapes */ ]
}room_users (sent to newly joined client)
{
"action": "room_users",
"users": [
{ "userId": "uuid", "username": "bob", "color": "#3498DB", "canEdit": true }
],
"myUserId": "uuid",
"myColor": "#E74C3C",
"myUsername": "alice",
"canEdit": true
}user_joined (broadcast to existing clients)
{
"action": "user_joined",
"userId": "uuid",
"username": "alice",
"color": "#E74C3C",
"canEdit": true
}error
{
"action": "error",
"message": "You do not have permission to edit this room.",
"code": "PERMISSION_DENIED"
}SystemSketch uses a three-tier permission system stored in the room_permissions table.
| Level | Read | Draw / Edit | Save | Rename | Toggle Visibility | Delete | Invite / Revoke |
|---|---|---|---|---|---|---|---|
| VIEWER | β | β | β | β | β | β | β |
| EDITOR | β | β | β | β | β | β | β |
| OWNER | β | β | β | β | β | β | β |
- The room creator is automatically the
OWNER. - Public rooms allow anonymous read access (via REST and WebSocket as
VIEWER). - Private rooms block all anonymous access; only users with an explicit permission row may enter.
The frontend is a Vue 3 SPA built with Vite.
cd frontend
npm install
npm run dev # Dev server at http://localhost:5173
npm run build # Production build β dist/
npm run test # Run Vitest unit tests
npm run lint # ESLint check| Route | View | Description |
|---|---|---|
/ |
HomeView |
Landing page |
/login |
LoginView |
JWT login form |
/register |
RegisterView |
User registration form |
/rooms |
RoomsView |
Browse and create rooms |
/workspace/:id |
WorkspaceView |
Canvas workspace with real-time collaboration |
cd backend
# Activate virtual environment first
pytest tests/ -v --cov=app --cov-report=term-missingTests use an isolated async test client with a separate in-memory SQLite database seeded per test via fixtures in tests/conftest.py.
cd frontend
npm run test # Run all Vitest unit tests
npm run test -- --coverage # With coverage reportTest files live in frontend/src/test/ and cover stores (auth, canvas, room) and key views.
| Branch | Purpose |
|---|---|
main |
Production-ready, protected |
setup/project-structure |
Initial infrastructure scaffolding |
feature/websocket-collaboration |
Core WebSocket implementation |
feature/jwt-authentication |
JWT auth system |
feature/undo-redo-permissions |
Undo/redo and permission system |
This project follows Conventional Commits:
<type>(<scope>): <short description>
[optional body]
[optional footer]
| Type | When to use |
|---|---|
feat |
A new feature |
fix |
A bug fix |
docs |
Documentation only changes |
chore |
Build process, dependency updates, config |
test |
Adding or updating tests |
refactor |
Code change that neither fixes a bug nor adds a feature |
perf |
Performance improvement |
Examples
feat(rooms): add public/private visibility toggle
fix(websocket): handle disconnect before accept gracefully
docs(readme): add full API reference
test(auth): add refresh token rotation test
Contributions are welcome and appreciated. Please follow these steps:
git clone https://github.com/<your-username>/SystemSketch.git
cd SystemSketchgit checkout -b feature/your-feature-nameFollow the Local Development instructions above.
- Keep changes focused on a single concern per PR.
- Follow the existing code style (Black + isort for Python, ESLint for TypeScript).
- Add or update tests for any changed behavior.
- Update documentation if your change affects the public API or configuration.
# Backend
cd backend && pytest tests/ -v
# Frontend
cd frontend && npm run testgit add .
git commit -m "feat(scope): describe your change"
git push origin feature/your-feature-name- Target the
mainbranch. - Fill out the PR template (describe what, why, and any testing notes).
- Link any related issues.
| Language | Formatter | Linter |
|---|---|---|
| Python | Black | Ruff |
| TypeScript / Vue | Prettier | ESLint |
Open a GitHub Issue and include:
- A clear, descriptive title.
- Steps to reproduce (ideally a minimal reproduction).
- Expected behavior vs. actual behavior.
- Environment details (OS, Python version, browser).
- Relevant logs or screenshots.
This project is licensed under the MIT License β see the LICENSE file for details.
Atul Mishra
- GitHub: @Atulmishra22
Built with β€οΈ for developers who sketch systems.