How we built a construction management platform where every estimate, contract, and payment has to be traceable across three companies from one login
A Las Vegas general contractor asked us to replace his disconnected tools with a single platform for estimating, subcontracting, invoicing, and paying subcontractors across three separate construction companies.
The simple version of the brief: automate estimate creation from plan documents, generate legal subcontracts from estimate line items, track invoices and payments, and sync everything to QuickBooks Online.
The version that actually mattered: three legally separate companies share one login and toggle between them in a header dropdown. Every number that leaves the system, whether a job number, estimate total, subcontract value, invoice, or payment, has to land in the right company’s QuickBooks with no duplicates, no cross contamination, and no rounding errors. A cost code that drifts between the system and QuickBooks turns into a job costing report that says the wrong thing, and a job costing report that says the wrong thing means the contractor doesn’t know if he made money on the project. In construction, that’s how companies go under.
This case study covers the architectural decisions that made the platform safe to run a real construction business on. It is the set of calls that separate a production tool from a prototype.
Who this was for and why it was hard
The client is a Las Vegas general contractor operating three companies: a general contracting firm (commercial and residential), a metal fabrication shop, and a new residential custom homes company. One person, one estimator, three different business entities with different estimate formats, different contract templates, different QuickBooks accounts, and different insurance requirements.
Their entire operation ran on a mix of PDF templates, an existing project management tool they’d outgrown, QuickBooks, Outlook inboxes, and an AI note taking app for site walks. The compounding problems were predictable: invoice and subcontract numbers duplicating across companies and confusing QuickBooks, cost codes multiplying into hundreds of near duplicates with nobody picking the right one, manual PDF generation for every contract, no connection between the estimate that won the job and the subcontracts that executed it, and zero visibility into which line items had been assigned to which subcontractor.
The brief was to build a platform where the entire lifecycle, from a site walk to a paid subcontractor, runs through one system, with each company’s data fully isolated and its own numbers, branding, and legal terms applied automatically. The harder requirement, stated plainly by the client on day one, was that the system had to prevent the numbering and cost code chaos that had plagued every other tool they had tried. Simple, he said, is better.
We delivered the platform over a focused 35 day build with a 5 day testing buffer. Three companies, five modules (estimator, subcontracts, invoices, payments, photos), integrations with QuickBooks Online and Microsoft Outlook, and an AI powered estimator that reads construction plans and site walk notes.
At a glance
- Client: Las Vegas general contractor, three companies
- Scope: Multi company construction management platform, company toggle
- AI: Claude and Gemini for plan extraction and estimate skeleton generation (human review required)
- Evidence: Per company atomic numbering, Decimal money, audit trail on every financial action
- Stack: Next.js, NestJS, PostgreSQL with Prisma, BullMQ workers, Puppeteer PDFs, Stripe Connect, S3
Decision 1: Three companies in one login, isolated at the database layer
The platform serves three legally separate companies from one interface. A dropdown in the header toggles between them. Every module, from estimates and subcontracts to invoices, payments, and photos, switches to that company’s data instantly.
The standard way to implement this is application level filtering. Every query includes a company filter. The application is responsible for never forgetting that filter. This works until a developer adds a feature, forgets the filter on one query, and an estimate from Company A shows up in Company B’s list. In a normal SaaS this is a bug. When Company A and Company B are separate legal entities filing separate tax returns with separate contractor licenses, it is a compliance event.
We had been burned by this failure mode on a previous multi tenant build. The same email address used across two tenants caused data leakage because the application assumed email uniqueness was global when it should have been per tenant. We were not going to repeat that.
We shipped two layers of isolation.
The first is a Prisma client extension that automatically injects the active company ID into every database query: reads, writes, counts, deletes. A developer cannot write a query against a tenant owned table without the company filter. It is injected by the framework, not by memory. The extension also filters out soft deleted records on every read, closing another class of accidental data exposure.
The second is PostgreSQL Row Level Security on every tenant owned table. Even if the application layer fails, the database refuses to return rows that don’t belong to the authenticated company. The two layers are redundant by design: either one alone would prevent the leak, both together make it structurally impossible.
Company access is verified on every authenticated request against live database membership, not the JWT payload. If a user’s access to a company is revoked, their existing token cannot access that company’s data on the very next request.
For a contractor running three legal entities, the architectural claim is precise: toggling between companies changes everything you see, and it is not possible for data from one company to appear in another through any application bug, query mistake, or stale authentication state.
Decision 2: The AI builds the estimate skeleton, the estimator sets the prices
Construction estimating is not a data entry problem. It is a judgment problem. A general contractor walks a site, takes notes, reviews plans, talks to subcontractors, and produces a division by division cost estimate from experience. No document the contractor uploads contains the prices. The plans show what the building needs. The sub bids show what the trades will charge. The contractor’s experience fills in the rest.
The original spec described an AI estimator that would scan architectural drawings and generate cost estimates from Las Vegas market rates. After studying the client’s actual estimates, site walk notes, and plan sets, we realized this was the wrong product. The prices are not in the documents. They are in the contractor’s head and in the sub bids that come in over email. An AI that guesses prices produces estimates the contractor cannot trust.
We built the AI to do what it is actually good at: turning messy, unstructured input into a structured estimate skeleton.
The contractor uploads whatever he has: site walk notes from an AI transcription app, a rough floor plan sketch, or a full 60 page architectural plan set. The system extracts text from digital PDFs, detects scanned documents and routes them through OCR, scores every page on data density using structural signals, and distills the kept pages down to schedule tables, area calculations, and quantity takeoffs. The distilled data goes to a capable AI model in small token budgeted batches, merged afterward with deduplication so the model stays accurate on each chunk.
The AI produces grouped line items with descriptions, scope narratives written in professional estimating language, quantities pulled from the documents where clearly stated, and cost code tags from a controlled master list. It never guesses a quantity it cannot find. It never generates a price. Every line lands in a review step where the estimator confirms or corrects before entering the pricing grid.
Three companies use the same engine, but each company’s estimate renders differently. The GC’s estimates group by construction division. The fabricator’s estimates group by scope item. The system detects which company is active and applies the correct structure, branding, contract terms, and proposal format automatically.
The principle: AI is best for turning unstructured input into structured output and worst for authoritative numbers. Use it where it has an edge and replace it with human judgment where it doesn’t.
Decision 3: One cost code list, picked not typed, QuickBooks authoritative
The client’s single biggest pain point, stated in his own words: “The issue we have currently is making sure the invoice, subcontract, and job numbers don’t duplicate or confuse QB. Cost codes start to double and repeat between QB and the system, leaving hundreds of bad ones and no one picks the right ones.”
This is the most common failure mode in construction software. Every tool maintains its own cost code list. QuickBooks maintains another. They drift. Nobody knows which is canonical. Job costing reports pull from the wrong codes and show meaningless numbers.
We made three calls to prevent this.
One shared master list across all three companies. A company can add a code, but it joins the shared list. There are no per company copies that can drift independently.
Pick only input. Cost codes are always selected from a searchable picker, never free typed. A user cannot create three different spellings of the same code.
QuickBooks as source of truth. The platform maintains a local mirror of QB’s Item list via Change Data Capture. Pushes match by ID and never auto create near duplicates. Unmatched items are flagged for human mapping.
The architectural result: cost codes cannot multiply. They are controlled at the source, enforced at the input, and matched by ID on sync.
Decision 4: Estimate line items lock to one subcontract, and no payment goes out without a signed contract and current insurance
Construction money flows through a specific chain: an estimate wins the job, line items become subcontracts for each trade, subs do the work and submit invoices, and the contractor pays the subs. Every link has a rule that, if violated, creates a real business or legal problem.
Cost, not markup. The subcontract value comes from the estimate’s cost column, never the marked up client price. The markup gap is the contractor’s profit margin. The system enforces this in code. The markup percentage is never exposed in any subcontract facing output.
One line, one sub. A line item can only be assigned to one subcontract. Assigned lines are struck through and disabled in the picker. Voiding a subcontract releases the lines. The estimate detail shows assignment progress.
Signed and insured before payment. The client requested this mid build: no payment goes out unless the subcontract is signed (EXECUTED status) and the sub’s insurance certificate is on file and not expired. The payment guard runs at invoice approval and again at payment initiation. Insurance that expires mid project holds all pending payments until a current certificate is uploaded.
These are not edge cases. They are the rules that protect a contractor from paying for uncontracted work, from liability exposure when a sub’s insurance lapses, and from accidentally revealing profit margins.
Decision 5: Per company atomic numbering that QuickBooks can trust
The client flagged this on day one. Invoice numbers, subcontract numbers, and job numbers were duplicating across his three companies and confusing QuickBooks.
We built a NumberSequence table with atomic per company counters for every entity type: jobs, estimates, subcontracts, invoices, and payments. Each company’s sequence is independent. Number assignment runs inside a database transaction with row level locking. Two users creating invoices at the same second cannot receive the same number.
Before launch, the client provides the starting numbers from his existing systems. The sequences pick up where the old system left off. Every number that reaches QuickBooks carries the platform’s ID alongside it, and the platform stores QuickBooks’ internal ID back. That one to one binding prevents sync duplicates.
The architectural claim: it is not possible for two records in the same company to receive the same number, for a number from one company to appear in another’s QuickBooks, or for a sync to create a duplicate entry.
Decision 6: Money is Decimal, never floating point, everywhere
Construction estimates routinely reach six and seven figures. A $642,274 restaurant buildout broken into 40 line items with per line markup, project level overhead, profit, contingency, and bond percentages applied in layers, then retainage held on subcontractor payments. The math compounds. A floating point rounding error on one line propagates through every calculation downstream.
Every money field in the database is Prisma Decimal with 12 digits of precision and 2 decimal places. Every calculation goes through a single money helper module that uses Decimal arithmetic with banker’s rounding. The frontend never performs money math. It displays numbers from the backend, which recomputes authoritatively on every change.
The principle: money math happens in one place, in one type, with one rounding rule. If you can grep the codebase for native arithmetic on currency and find zero hits, the system is safe.
What the platform looks like in production
The contractor logs in and sees a dashboard: active projects, estimates needing attention, subcontracts pending signature, overdue invoices, insurance certificates expiring within 30 days. A company toggle in the header switches the entire view.
He opens a new estimate, drops in his site walk notes and the plan set, optionally tells the AI to focus on a specific scope, and clicks Generate. A background worker scores, filters, and batches the documents, then produces a grouped estimate skeleton with scope descriptions and quantities. He reviews the AI’s work, corrects what is wrong, and moves to the pricing grid.
He generates a professional proposal PDF with his company’s branding, his terms, and his format, and sends it to the client. The client approves. He selects line items from the approved estimate, picks a vendor, and creates a subcontract. The assigned line items are struck through on the estimate. The system generates a multi page legal contract PDF. He sends the contract link to the subcontractor. The sub signs on screen, and the signature is embedded into the contract PDF.
The sub uploads an insurance certificate. When the sub submits an invoice, the contractor approves it, but only if the contract is signed and the insurance is current. He schedules the payment, confirms it, and the subcontractor’s paid amount and invoice status update automatically. The same flow runs independently on all three companies. Different branding, different numbers, different QuickBooks accounts, same login.
Standards, architecture, and controls
- Multi company isolation: Prisma extension + PostgreSQL RLS + live membership verification
- AI document extraction with human in the loop review before data enters the estimate
- Hybrid extraction pipeline: rule based structural scoring + token budgeted batching + OCR for scanned documents
- Cost code governance: one shared list, pick only, QB source of truth via local mirror and CDC
- Per company atomic numbering: transaction locked sequences for all entity types
- Decimal money: Prisma Decimal(12,2), one money helper, banker’s rounding, no float in financial path
- Payment guard: signed contract + current insurance required at approval and payment
- Line item tracking: one estimate line to one subcontract, struck through and locked, released on void
- Audit logging: every financial state change with actor, timestamp, action, entity reference
- Soft deletes: estimates, subcontracts, invoices never hard deleted
- Idempotency: unique keys on payments, Request Id headers on QB writes
- E signature: canvas capture, IP logging, timestamp, embedded in contract PDF
What we would tell the next team building a construction management platform
A few things we learned that are not obvious until you build for a real contractor.
- The estimator is not a calculator. It is a document to structure tool. The AI’s job is to turn messy inputs into a grouped skeleton. The prices come from the contractor. Every team that tries to build an AI that generates prices ships a tool the contractor does not trust.
- Cost code chaos is the silent killer. It doesn’t crash or throw errors. It slowly makes every report meaningless. The fix is a controlled list with pick only input and one source of truth.
- Multi company is not multi tenant with different logos. Each company has its own estimate structure, contract templates, QB connection, numbering, and insurance requirements. Build this from day one or rebuild everything later.
- The payment guard is the feature the contractor didn’t know he needed. No payment without signed contract and current insurance. Once he has it, every other tool feels reckless.
- Construction plans vary wildly. A restaurant comes with 60 page D size sets. A bar remodel comes with walk notes and a sketch. Build a good enough extractor with a great review step, not a universal parser.
- Money math is not optional to get right. A contractor’s profit margin lives in the gap between sub cost and client price. A float error shifts that gap invisibly. Use Decimal from day one.
How to talk to us about a build like this
If you are a general contractor, subcontractor, or construction technology company replacing disconnected tools with a platform that ties estimating to subcontracting to invoicing to payment, or if you are evaluating where AI fits safely in a construction workflow, we offer a paid discovery engagement that produces an architecture document, an integration map for your QuickBooks and Outlook setup, and a build plan you own whether or not you continue with us.
We have shipped multi tenant construction platforms, AI document extraction pipelines for plan sets ranging from 6 to 66 pages, and QuickBooks integrations that solve the cost code duplication problem structurally rather than with sync rules. The discovery engagement is the fastest way to know what your platform actually needs before committing to a build.
