An open-source, real-time collaborative spreadsheet web app built as a monorepo.
- Real-time multi-user editing via WebSockets (Socket.IO)
- Canvas-based grid engine for high-performance rendering
- Per-cell formatting (bold, alignment, text/background color, font size)
- Multiple sheet tabs per grid with per-sheet row/column sizes and layout persistence
- Presence indicators and per-cell locking with conflict prompts and presence bubbles
- Auth (register/login/JWT), personal grid creation, share with collaborators (owner-only invite), owner-only delete
- Responsive UI with Tailwind CSS + Headless UI
- Right-click (context menu) on cells for common actions and formatting
- Demo Mode for local UI exploration without backend (see below)
collab-grid/
├── packages/
│ ├── client/ # React 18 + TS + Vite + Zustand + React Query + Tailwind
│ ├── server/ # Node.js + Express + TS + Socket.IO + Redis + MySQL
│ └── shared/ # Shared types/constants between client & server
├── infrastructure/ # (reserved) docker/k8s/monitoring configs
├── scripts/ # helper scripts
├── docker-compose.yml # Local dev infra (MySQL/Redis/MinIO) and app
├── package.json # npm workspaces
└── README.md
- Frontend: React 18, TypeScript, Zustand, React Query, Vite, Tailwind CSS, Headless UI
- Backend: Node.js, Express, TypeScript, Socket.IO, Redis (presence/locks/cache), MySQL (primary data)
- Infra: Docker + Docker Compose (optional), Nginx (later), MinIO (reserved)
- Auth: register, login, JWT session; change display name/password
- Grid: create/list/get/delete/rename
- Sheets: create/list/rename/delete, default layout on create
- Realtime: cell update/style, grid resize/dimension, presence (colored dots), per‑cell lock acquire/renew/release
- Formatting: bold, align left/center/right, text color, background color, font size
- Layout: drag to resize row height and column width per sheet; real‑time broadcast; persistence to MySQL
- Multi-line cell input: Shift+Enter for newline, Enter to submit
- Ownership and sharing: owner can invite collaborators; collaborators cannot delete or invite others
- List metadata: creation time, last modified, last editor (nickname)
- CORS: dynamic origin reflection for local IPs; client/WS URLs auto-derive from current host
- Context menu: copy, cut, paste, basic formatting, insert/delete row/col, and more
- Collaboration UX:
- Single editor per cell (locking). Others get a refusal toast if they try to edit.
- Presence bubble near the active/edited cell shows “N people also here” or the collaborator’s name.
- Update bubble/toast appears when a cell is changed by someone else.
- If no token is present, the client auto‑sets a
demo-token-*and runs in Demo Mode. - Demo Mode behavior:
- API calls short‑circuit with mock data (no server writes).
- WebSocket (realtime/locks/presence/persistence) is disabled.
- Use for quick UI preview only. For full realtime + persistence, log in normally.
- Node.js >= 18
- npm >= 9
- MySQL 8.x
- Redis 7.x
- (Optional) Docker Desktop or Colima for Docker Compose
- Install dependencies
npm install- Start required services yourself
- MySQL at
127.0.0.1:3306with databasecollabgrid - Redis at
127.0.0.1:6379
- Seed database (creates schema and demo users)
cd packages/server
npm run seed
# Users: [email protected] / pass1234, [email protected] / pass1234- Start backend
cd ../../
npm run dev:server
# API: http://localhost:4000/healthz- Start frontend
npm run dev:client
# Web: http://localhost:5173- Login & test
- Login at
http://localhost:5173/login - Create a grid, rename it, edit cells with formatting, add sheets
- Open two browsers (or two accounts) to test:
- Presence/locking: A starts editing A1; B tries to edit A1 gets a lock warning. A’s cell shows presence bubble if B is also focused there.
- Realtime update bubble: A edits A1; B sees a short toast “单元格 A1 已更新”.
- Row/Col resize: drag headers in A; B sees sizes update in real time; refresh either page and sizes persist.
This spins up MySQL, Redis, MinIO (reserved), server, and client.
npm run dev:infra
# then in another terminal
npm run dev:server
npm run dev:clientNotes:
- The compose file exposes MySQL 3306, Redis 6379, server 4000, client 5173.
- Server uses
DATABASE_URL=mysql://collab:collab@mysql:3306/collabgridandREDIS_URL=redis://redis:6379inside the network.
- Server
PORT(default 4000)DATABASE_URL(e.g.mysql://root:@127.0.0.1:3306/collabgrid)DB_REQUIRE_MYSQL(truein dev to force MySQL)REDIS_URL(e.g.redis://localhost:6379)JWT_SECRET(optional; defaultdev_secret_change_me)
- Client
VITE_API_BASE_URL(optional; defaulthttp://<current-host>:4000)VITE_WS_URL(optional; defaulthttp://<current-host>:4000)
- Auth
POST /api/auth/register{ email, password, displayName }POST /api/auth/login{ email, password } -> { token, user }PATCH /api/auth/meAuthorization: Bearer -> { displayName?, password? }
- Grids
POST /api/grids/Authorization: Bearer { title }GET /api/grids/Authorization: Bearer -> list with metadataGET /api/grids/:idAuthorization: Bearer -> grid + cells + sheets + layoutPATCH /api/grids/:idAuthorization: Bearer { title }DELETE /api/grids/:idAuthorization: Bearer (owner only)
- Sheets
GET /api/grids/:id/sheetsAuthorization: BearerPOST /api/grids/:id/sheetsAuthorization: Bearer { name }PATCH /api/grids/:id/sheets/:sheetIdAuthorization: Bearer { name }DELETE /api/grids/:id/sheets/:sheetIdAuthorization: Bearer
- Client -> Server:
grid:join{ gridId, sheetId } - Server -> Client:
grid:snapshot{ rows, cols, rowHeights, colWidths, cells } - Broadcast:
grid:operationforcell:update{ row, col, value }cell:style{ row, col, style }grid:dimension{ rows?, cols? }grid:resize{ rows?, cols?, rowHeights?, colWidths? }
- Presence
cell:focus/cell:blur
- Lock
cell:lock:acquire/cell:lock:renew/cell:lock:releasecell:lock:granted/cell:lock:denied/cell:lock:released
- The client auto-derives API/WS URLs from
window.location.hostnamewhen env is absent, enabling LAN access without CORS pain. - Row/column sizes are stored per-sheet in MySQL table
grid_sheet_layout. - Cell values and styles are stored in
grid_cells(pergrid_id,sheet_id,row_index,col_index). - Last modified and last editor are tracked in
grids.last_modifiedandgrids.last_editor_id. - The server persists sizes on
grid:operationwith typegrid:resize/grid:dimensionand broadcasts fresh snapshots as needed.
- Port already in use (4000): kill the process or use
npx kill-port 4000. - CORS errors on LAN: ensure server CORS reflects origin (already configured), and client uses LAN IP.
- MySQL keyword issues: columns
rows/colsare backticked; ensure your MySQL is 8.x and schema up to date. - Layout not persisting:
- Ensure you are NOT in Demo Mode (login required for persistence/realtime).
- Confirm
grid:operationpayload contains a validsheetIdand the server logs don’t show authorization issues. - Verify MySQL/Redis are running; table
grid_sheet_layoutshould contain a row for the sheet.
- Nicknames not showing: ensure clients pass JWT in WS auth; Redis directory keys are
grid:{gridId}:user:{userId}.
MIT © CollabGrid Contributors