The Problem
A mid-size operations firm of around forty staff was running its entire business across roughly fifteen interlinked Google Sheets. Job intake lived in one workbook, scheduling in another, time logging in a third, invoicing in a fourth, and a master dashboard pulled selected ranges from all of them via fragile cross-sheet references. The spreadsheets had grown organically over six years, and nobody in the business could draw a clean diagram of how data flowed between them.
The day-to-day cost was constant low-grade chaos. A reference broke when someone renamed a tab; a job logged in one sheet did not appear in the dashboard because a filter row had been deleted; two team members updated the same cell within seconds and one set of changes was silently overwritten. The operations manager spent two to three hours every Monday morning reconciling the previous week’s numbers — work that produced nothing new, only confirmed what had already happened.
The deeper problem was that the spreadsheets had passed the threshold where they could be fixed. Adding a column meant updating five formulas in three other workbooks. Onboarding a new staff member required half a day of “here is which sheet to look at for what” walkthroughs.
The Approach
We mapped the existing spreadsheet estate first — every workbook, every cross-sheet reference, every column that fed a downstream calculation. The map made it clear which sheets were doing real work and which were duplicates, abandoned experiments, or legacy artefacts. About a third of the columns were not used by anything.
From that map we designed a relational data model behind a custom Laravel application — jobs, clients, staff, time entries, invoices, and the relationships between them. The platform was built incrementally and rolled out one module at a time, starting with job intake (the spreadsheet most prone to breaking) and ending with the reporting layer. Each module shipped with a one-way import from the legacy spreadsheets and a parallel-running period so the team could compare outputs before retiring the old sheet.
The System We Built
A custom internal platform with a relational database underneath, accessed through a role-aware web interface. Job intake captures structured data with validation. Scheduling, time logging, and invoicing all read from the same job records — no more typing the same job reference into four different sheets. A reporting layer replaces the master dashboard with live queries against the database rather than fragile cross-sheet references. Audit logging records who changed what, so concurrent edits no longer overwrite each other silently.
The Outcome
The Monday-morning reconciliation disappeared. The operations manager now spends that time on actual planning rather than tying out numbers. Onboarding a new staff member dropped from a half-day spreadsheet tour to a thirty-minute walkthrough of one interface. The team has stopped working around broken references because there are no references to break.
Less visible but more significant: the business can now ask questions of its own data that were previously impossible. “How many jobs of type X did we run last quarter, and what was the average time-to-completion?” used to require an afternoon of manual spreadsheet work. It is now a saved report.
What We Learned
The instinct on these projects is to rebuild everything at once. We resisted that and shipped one module at a time with overlap periods, and it made the migration uneventful rather than dramatic. Nobody had to take a leap of faith — they saw the new module produce the same outputs as the old sheet for two weeks, then switched.
Have a Similar Migration Ahead?
If your business is running on spreadsheets that have grown past the point of comfortable maintenance, get in touch and we can walk through what an incremental migration would look like for your operation.