Skip to content

Repository files navigation

spreadsheet-collab

A real-time collaborative spreadsheet editor built from scratch. Multiple users can edit the same grid simultaneously, formulas recalculate automatically across dependent cells, and every change syncs instantly over WebSockets.


Why I built this

I wanted to understand what actually makes Google Sheets hard to build — not the UI part, but the engine underneath. How do you handle 50 people editing the same cell at once? How do you recalculate a formula when the cell it depends on changes, without recalculating everything? How do you keep undo/redo working when edits are coming in from multiple clients?

This project is my attempt to answer those questions with working code.


What it does

  • Formula engine across a 2,600-cell grid (52 columns x 50 rows) supporting SUM, AVERAGE, MIN, MAX, COUNT, IF, CONCAT, LEN, and TRIM. Formulas that reference other cells recalculate automatically when the source cell changes.
  • Real-time sync over Socket.IO. Edits broadcast to all connected clients instantly. Cursor positions are also broadcast so you can see where other users are working.
  • Optimistic updates so your own edits appear immediately without waiting for the server to confirm. The server is authoritative, so if there's a conflict the server state wins and the client reconciles.
  • 50-state undo/redo that tracks the full history of changes including formula edits.
  • CSV and Excel export via the xlsx library.
  • Cell styling — bold, italic, text alignment, background color.
  • Docker support for one-command local setup.

The hard part: formula dependency resolution

The naive approach to formula recalculation is to re-evaluate every formula in the grid whenever any cell changes. That works fine for small grids but falls apart at scale.

The formula engine here builds a dependency graph. When you enter =SUM(A1:A5) in B1, B1 gets added as a dependent of A1 through A5. When A3 changes, only the cells that depend on A3 get re-evaluated — and they get evaluated in topological order so a formula that references another formula always sees the updated value.

Circular references are caught before evaluation starts.


Real-time architecture

Client A         Server              Client B
   |                |                   |
   | edit cell  --> |                   |
   |                | broadcast to  --> |
   |                | all clients       |
   | (own edit      |                   | (receives
   |  already       |                   |  update)
   |  applied       |                   |
   |  locally)      |                   |

The server holds the canonical grid state. When a client sends an edit, the server applies it to its own state and broadcasts the update to every connected client. Clients apply the update to their local state.

Cursor positions work the same way — each client sends their current cell on focus and the server rebroadcasts to everyone else.


Tech stack

Backend

  • Node.js + Express
  • Socket.IO
  • Custom formula engine (no external formula library)

Frontend

  • React
  • CSS (DaisyUI)
  • Socket.IO client
  • xlsx library for export

Infrastructure

  • Docker + docker-compose (multi-stage build)
  • Deployed on Render (render.yaml included)

Running locally

One command with Docker:

docker-compose up --build

Or manually:

npm run install-all
npm run dev

Server runs on port 5000. Client runs on port 3000.


Project structure

server/
  index.js       Express server + Socket.IO setup
  socket.js      WebSocket event handlers and state management
  models/        Cell and grid data models
  utils/         Formula engine, dependency graph, cell parser
  routes/        REST API routes (export, grid state)

client/
  src/
    components/  Grid, Cell, Toolbar, CursorOverlay
    hooks/       useSocket, useFormula, useHistory
    utils/       Formula parser, cell address helpers

What I would do differently

  • The formula parser is a hand-rolled recursive descent parser. It works but doesn't handle edge cases like nested IFs cleanly. I'd replace it with a proper AST-based parser.
  • Undo/redo doesn't handle concurrent edits correctly. If two users make changes at the same time, undoing on one client can put the other client's state in an inconsistent position. Operational transformation or CRDTs would solve this properly.
  • The server holds all grid state in memory. For production you'd want a database-backed persistence layer with snapshotting so state survives restarts.

About

Real-time collaborative spreadsheet editor with a custom formula engine, dependency graph recalculation, optimistic updates, and 50-state undo/redo over Socket.IO.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Contributors

Languages