Eric Berglund · coloredrock
01 Case Study

Replacing an 11-tab pool estimating spreadsheet with a custom ops app

On a custom pool, one wrong number in the estimating workbook cascades into the signed quote. A SoCal pool builder was running every pricing formula out of 11 Excel tabs. I replaced the workbook with a custom estimating and operations platform. Single source of truth, drift detection, customer owns the source code.

11 → 1
Excel tabs
collapsed into one app
3
Phases shipped after Phase 1
customer kept buying
Owned
Source code, data,
and runbook
No
Dedicated server bill
at this traffic level
11-tab workbook Custom app
Quote build Half-day of cross-tab hunting Under an hour
Validation Typos save silently Stops missed items at save
Supplier prices Stale, manually edited Auto-propagating with drift alerts
Operability Owner-only Staff can do first pass
Audit trail None Every price change timestamped

§ 01 · The problemThe estimating spreadsheet was the company

When the customer first walked me through their day, the operations stack was a single Excel workbook. 11 tabs, hundreds of line items, and the whole company’s pricing logic baked into it. Steel, Excavation, Plumbing, Gas, Electrical, Shotcrete, Tile & Coping, Equipment, Plaster, Options, Soft Costs. One tab per trade, formulas chained across all of them, and at the end a “Break Out Price List” tab that summed it all into a customer-facing number.

Quoting a custom pool meant opening the workbook, hunting for the right line items across all 11 tabs, hand-keying quantities, hoping no formula had been overwritten since the last quote, and trusting that the prices in those cells reflected what suppliers were actually charging this month. The tabs had real math. Rebar count from the square root of interior area, shotcrete yards as IA × 0.032, plaster tier matched by interior-area bracket and perimeter bracket. And any one of those formulas could silently get clobbered when someone clicked into the wrong cell.

One bad number compounds. A unit-price typo in one tab cascades through every line item it touches, then into the project total, then into the signed quote. The customer signs. The job runs. The real costs come in. Whoever did the math eats the difference.

The 11-tab structure had real costs. Missed line items. Formulas any wrong click could clobber. Supplier prices in cells that went stale. A workbook only the owner could safely operate, which made estimating a bottleneck on his calendar.

Formulas, ported literally from the workbook
rebar_count = ⌈ √(interior_area) ⌉
shotcrete_yds = interior_area × 0.032
plaster_tier = bracket(IA) × bracket(perimeter)

The wedge here was straightforward: replace the workbook with one app that holds every line item once, recalculates derived values automatically, tracks every price change with a timestamp, and warns you on the summary page if a quote you’re looking at has prices that drifted from the current book. The customer would own the source code, the data, and the database. No subscription.

Before 11-tab workbook
B7 fx =SQRT(IA_TAB!B4)*REBAR_PER_SF
A
B
C
5
Steel
=SQRT(IA)*REBAR
$####
6
Excavation
=L*W*0.85
$####
7
Shotcrete
=IA*0.032
$####
8
Plaster
=VLOOKUP(IA,...)
$####
9
Tile
=PERIM*RATE
$####
Steel Excavation Plumbing Gas Electrical Shotcrete Tile Equipment Plaster Options Soft Costs Break Out
After one app
Estimator · Project B All trades
Steel · #4 rebar qty = ⌈√IA⌉ $####
Excavation L × W × 0.85 $####
Shotcrete IA × 0.032 yd $####
Plaster (tier 4) bracket(IA, P) $####
One schema · 11 trades Audit on every change

Same pricing logic, two failure modes. The workbook spreads it across 11 tabs. One stray click can clobber a formula and silently break next month’s quote. The app holds every line item once, derives values from project dimensions, and writes a row to price_history every time a price changes.

Why not just buy something?

Buildertrend, JobTread, Houzz Pro, Projul. The off-the-shelf options for a builder this size. All solve a broader workflow than the actual problem here. The problem was narrow: preserve the existing estimating logic exactly, remove the spreadsheet’s failure modes, and make quoting faster without forcing the company into someone else’s process. A subscription tool would have flipped that around. The company bending its workflow to fit the software, paying every month forever, and still re-keying numbers because the tool didn’t quite know what a pool was.

The app was built around how the company already sold pools. Along the way we deliberately said no to features that look good on a competitor’s marketing page but create bloat at this company size: full Gantt scheduling, email marketing, daily field logs, vendor portals, lead scoring, revenue forecasting. Pool builders run on referrals, not drip campaigns. The result is a tool that does the customer’s work, not generic-contractor work the customer has to translate.

§ 02 · ScopeOne document. Signed before any code.

Every project starts with a SCOPE document. It’s short, opinionated, and names what’s out of scope as carefully as what’s in. For this engagement, the Phase 1 scope was the Estimator. Price book admin plus the dimension-driven quote builder, sharing one customers / projects / estimates model. Everything else (quote expiration, user roles, customer portal, dashboards) was named in the Phase 2 candidate list and explicitly deferred. Most of it later got built; some of it is still on the “never” list on purpose.

SCOPE.md excerpt
# SCOPE. Pool ops estimator ## Engagement - Customer: SoCal pool-construction shop - User: owner, admin, sales staff (3-person ops team) - Budget: [fixed price] - Delivery: [Phase 1 of multiple] ## The ask Replace the legacy pricing workbook. An 11-tab Excel file with ~1,000 line items. With a web app that: 1. Lives online, single source of truth, no version conflicts 2. Auto-calculates derived values (rebar count, shotcrete yards, tier matching) so formulas can’t get overwritten 3. Holds prices in one place; vendor changes propagate to every new estimate 4. Produces a clean break-out price list matching the layout the company already shows its customers Anti-positioning: not Buildertrend. Not a CRM. Not a Gantt chart. Estimate in minutes, not hours. ## In scope (Phase 1) - Price Book admin: 11 trades, ~257 line items, tiered pricing, 7-tier plaster grid - Estimator: pool-design-friendly inputs (area + perimeter direct, not L×W); auto-calc; category walkthrough; summary - Auto-calc rules ported literally from the spreadsheet - Saved estimates with status pipeline - Auth: username + password, session cookies - Stack: Cloudflare Worker + D1 + Pages, no build step - Deploy: customer’s own ops subdomain ## OUT of scope (Phase 2 or never) - Quote expiration + price drift. Phase 2 - User management + roles. Phase 2 - Customer + Project layer. Phase 2 - Customer-facing portal. Phase 2 - Dashboard. Phase 2 - Full Gantt scheduling. Never - Email marketing / drip campaigns. Never - Vendor / sub portals. Never - Lead scoring / sales pipeline viz. Never

The OUT-of-scope list is doing real work here. Naming the Phase 2 candidates kept the engagement honest about what Phase 1 was; naming the “never” items kept the customer’s tool from drifting into generic-contractor software.

§ 03 · PlanThe implementation plan, before any code

After SCOPE, I write the PLAN. It’s the implementation strategy. Concrete enough to argue with, abstract enough to not pretend to be code. The customer reviews it, pushes back, signs off. Then I start building.

PLAN.md excerpt
# PLAN. One Worker, one database, one HTML page ## Architectural decisions 1. Single Worker, single D1, single HTML page. The whole tool fits in one Worker. No microservices, no separate auth service, no jobs queue. Everything in one runtime that fits in your head. 2. No build step. React loaded from CDN, Babel transpiled in-browser. wrangler deploy ships everything. New-developer onboarding is zero. 3. No ORM. SQL is hand-written. Schema is small enough to read at a glance; ORMs hide more than they help at this size. 4. Foreign keys enforced everywhere. Referential integrity is the database’s job, not the application’s. Impossible to forget. ## Build order (Phase 1) - Schema + migrations + seed from extracted spreadsheet data - Worker API: price book CRUD + estimate calc endpoint - Frontend: Price Book admin (CRUD UI for items, tiers, plaster grid) - Frontend: Estimator (dimensions → auto-calc → walkthrough → summary) - Match the spreadsheet’s “Break Out Price List” tab exactly - Side-by-side test against the spreadsheet on a real recent quote. Phase 1 isn’t done until numbers match ## Risks - Excel-formula fidelity drift → mitigation: every formula extracted and reproduced literally; numerical diff against the spreadsheet before declaring done - Price book stays stale (same failure as the spreadsheet) → mitigation: price_history audit table + drift detection on summary view (shipped in Phase 1.5)

§ 04 · BuildWhat we shipped

Quoting workflow, in five steps A five-step horizontal flow: input dimensions, derive values, select line items, check totals and drift, export quote PDF. 01 INPUT area + perimeter (no L × W) 02 DERIVE rebar · yards tier matching 03 SELECT 11 trades live prices 04 CHECK totals · drift detection 05 EXPORT PDF · pipeline tracked

Once dimensions are in, the system handles the math, the pricing, the drift check, and the PDF. The human picks which line items apply. The call that needs judgment.

Phase 1 shipped the Estimator and the Price Book. Phase 1.5 added quote expiration and price-drift detection. Phase 2 added user management and roles. Phase 3 added customers, projects, dashboards, and the client portal. Each phase was scoped, priced, and signed before the previous one shipped. The customer paying for what the next phase was worth before any of it got written. The discipline is the deliverable as much as the code is.

app.[customer-domain] / projects
Pool Ops
Dashboard Projects Price Book Customers Team
← Back to Project B. Pool + spa
1. Project Info
2. Items & Categories
3. Summary & Price
Items & Pricing
Overhead & Permits Excavation Steel Plumbing Gas Electrical Shotcrete Tile & Coping Equipment Plaster Options
Item & NotesQtyUnit PriceSubtotal
Overhead1 flat####$####Del
Engineering1 flat####$####Del
Permits1 flat####$####Del
+ Add Item
Category Subtotal$#,###
Previous Category Next Category Save & Continue to Summary

The Items & Categories step inside the estimator (demo data shown). Each of the 11 trades from the original Excel workbook is its own tab; line items pre-populate from the price book at their current rates and can be toggled in, adjusted, or noted per estimate. The 3-step wizard. Project Info, Items & Categories, Summary & Price. Carries every quote through the same path, every time.

app.[customer-domain] / price-book / steel
Pool Ops
Dashboard Projects Price Book Customers Team
Steel · price book
Items & Current Pricing
Item Unit Current Last changed
#3 rebar per LF $#### 2025-XX-XX Edit
#4 rebar @ 18″ OC per LF $#### 2025-XX-XX Edit
#4 rebar @ 12″ OC per LF $#### 2025-XX-XX Edit
Tie wire (16ga) per lb $#### 2024-XX-XX Edit
Chairs · 3″ plastic each $#### 2024-XX-XX Edit
+ Add Item
Every edit writes a row to price_history. That’s what makes drift detection on quotes possible later.

The Price Book screen, demo data shown. Every line item that an estimate can pull from lives here, in one place, at the current rate. The sidebar shows all 11 trades; the main view shows the items in the active trade with their current price and last-changed date. The audit trail is what later powers price-drift detection on saved quotes.

Spec at a glance

FrontendSingle static HTML page. React 18 + ReactDOM loaded from CDN. Babel transpiled in-browser. No bundler, no build step.
BackendSingle Cloudflare Worker. Routes manually dispatched (no framework).
DatabaseCloudflare D1 (SQLite at the edge). One schema, all foreign keys enforced.
AuthUsername + password, refresh-token rotation in HttpOnly cookies, no third-party auth provider.
Price freshnessEvery price change writes to a price_history audit table. Estimates lock prices at quote time and compare against the current price book on every read. Drift surfaces on the summary, never silently.
PDF exportPure HTML → print stylesheet. Quotes are real, paginated documents, not screenshots.
HostingCloudflare Pages + Workers. Deployed to the customer’s own ops subdomain. No dedicated server bill at the customer’s current traffic level.

§ 05 · ShipWhat changed for the customer

The app inverts each cost. Validations stop missed items at save. Calculated values can’t be clobbered because they’re computed, not edited. Supplier prices propagate, and the drift card warns when a sent quote has stale prices. The UI is clean enough that any staff member does the first pass and the owner reviews instead of building each one from scratch.

Phase 1 went live. The owner started using it for real quotes; the spreadsheet got archived. Phase 1.5 followed quickly. The first time someone updated a price in the price book that affected a quote already sent, the drift card on the summary made it impossible to miss. Phase 2 (users + roles) and Phase 3 (customers + projects + portal + dashboard) followed as the tool earned the trust to grow into more of the operation.

app.[customer-domain] / projects / Project B / quote-2 / summary
← Quote 2 of Project B
3. Summary & Price
2 line items have price drift since this quote was created
Steel · #4 rebar @ 18″ OC quote: $#### book now: $#### +8%
Plumbing · 2″ main, sched 40 quote: $#### book now: $#### +3%
Keep quoted prices Refresh from book
Subtotal$#####
Soft costs$####
Total$#####

The drift card on a quote summary, demo data shown. Every estimate locks the prices it was created with; on every read, the summary compares those locked prices against the current price book and surfaces any line that drifted. The user keeps the quoted prices or refreshes from the book. Explicitly, never silently.

1
Phase 1
Shipped
Estimator + Price Book
11 trades · auto-calc
PDF export
1.5
Phase 1.5
Shipped
Quote expiration
Price-drift detection
2
Phase 2
Shipped
User management
Roles + permissions
3
Phase 3
Shipped
Customers + projects
Dashboard
Client portal

The customer owns the source code, the data, and a runbook for keeping it running. Hosting runs on Cloudflare infrastructure with no dedicated server bill at this traffic level. What the customer bought was a one-time engagement they could come back to when they wanted more. Through every build phase, the customer talked to me. Design decisions, questions, changes. Phase 2 went to me, not a support queue. Same with Phase 3. Same person, same code, same number to call.

You have a software problem.

If your business is running on a spreadsheet everyone’s afraid to break. Or on subscriptions that still don’t fit how you work. Let’s talk. The first conversation is free. If it’s a fit, the next thing you’ll get is a written SCOPE document.

Get in touch →

Clients aren’t named on a public site. References available on request to qualified prospects.

← Eric Berglund · coloredrock.com Case study · 01