Eric Berglund · coloredrock
01 Case Study

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

A SoCal pool builder was quoting custom pools out of an 11-tab Excel workbook — hundreds of active pricing items, fragile formulas, and no reliable way to know when supplier costs had changed. I replaced the workbook with a custom estimating and operations platform that the customer owns outright.

§ 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.

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 SaaS 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

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
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

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 — not a subscription that bills whether they use it or not.

You’ve been pricing SaaS.

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