FE
Software Definition · Canonical Reference

Architecture
& Design

The single source of truth for how Field Engineer is built: the modular split that isolates each tool, the shared core they all depend on, the backend contracts, and the data models. For developers and architects.

Supersedes the architecture fragments in the Technical Reference & Stage 2 spec
What changed. This document adopts a modular split: each tool becomes its own independently deployable front end on a shared core library, rather than the single-file, multi-mode app described in earlier drafts. The goal is fault isolation — a failure in one tool cannot take down another. This resolves the open decision in Master Plan §14 ("modularize before Stage 3?"). The data models, recommendation logic, credential model, and backend contracts below are unchanged in behavior — only how the code is packaged and deployed changes.

Contents

  1. How to read this document
  2. The split architecture
  3. Module boundaries & fault isolation
  4. The shared core library
  5. The backend (stateless relay)
  6. Backend action contracts
  7. Data models
  8. Recommendation engine (Sales)
  9. Credential model & gate evaluation (Repair)
  10. Safety enforcement in app logic
  11. Security & privacy
  12. Storage & persistence
  13. Cross-tool integration (planned)
  14. Migration path & open decisions

01How to read this document

This is the canonical reference for architecture, data models, and backend contracts. Where the Technical Reference, the Stage 2 spec, or the User Guide describe the same model, they should point here — keeping one source prevents the drift the Master Plan warns about.

Status is tagged throughout so readers always know what exists versus what is designed: Built working & deployed · First pass built but intentionally shallow · Planned designed, not built · Future later stage.

02The split architecture

Field Engineer is a set of independently deployable tool apps, each consuming one versioned shared-core library, all talking to a single stateless backend. The customer-facing app is a separate build that never loads internal logic. Nothing in one tool can crash another.

UNIFIED SHELL / LAUNCHERone front door · routes to each tool app Sales / Surveyown deploy Pricingown deploy Tech / Repairown deploy ·holds safety logic Inventoryown deploy Customer Appseparate buildno internal logic SHARED CORE LIBRARY (versioned)design system · storage abstraction · backend client · nameplate read STATELESS BACKEND · /api/lookupholds keys · stores nothing · action-routed Anthropic (Claude)vision · diagnosis · search RentCastproperty records VendorsFerguson · Johnstone…

The three layers

03Module boundaries & fault isolation

The contract between modules is deliberately narrow: a tool app may depend only on the shared core and on backend actions. Tool apps never import each other. That single rule is what guarantees the blast radius of any failure stays inside one tool.

Tool appOwnsDepends onStatus
Sales / SurveySurvey flow, recommendation engine, rebate request UICore; backend property, utility, nameplate, rebates, parsebill, rates, estimateFirst pass
PricingPrice book, markup rules, owner/tech view toggleCore; backend parsepricesFirst pass
Tech / RepairDiagnostic loop, credential gating & hard-stop logic, job recordCore; backend diagnose, nameplateFirst pass
InventoryLocations, parts, per-location quantities, searchCore (local only in first pass)First pass
Customer AppPublic service requestCore (UI only); backend public actionsPlanned
Safety lives in Repair, not core. The credential gates and hard-stop logic (§09–§10) reside inside the Tech/Repair app, not in the shared core. This is deliberate: it means a Pricing or Sales change can never regress a CO or cracked-heat-exchanger hard stop, because that code is in a separately deployed bundle that those changes don't touch.
Customer-app boundary (unchanged, Master Plan §09). The customer app is a separate build that talks to the system only through the backend. A customer's browser must never load internal cost, margin, inventory, or credential logic. The split makes this boundary structural rather than a convention.

04The shared core library

Everything every tool needs, packaged once and versioned. Tool apps pin a core version; core ships changes deliberately so an upgrade is a conscious act per app, not an accidental break.

Core moduleResponsibilitySource today
Design systemFonts (Archivo / IBM Plex), palette, component styles, the app shell chromeShared across all four current docs
Storage abstractionPlatform store when present (Claude preview), localStorage fallback on the hosted site; per-key get/setTech Ref §04 (resolved)
Backend clientBuilds the action request, attaches the optional app token, posts to /api/lookup, returns clean JSON or a typed errorTech Ref §01–02
Nameplate readCaptures photos, downscales to thumbnail, sends full-res once for the AI read, returns structured fields + confidenceTech Ref §03, §08 (shared by Sales & Repair)
core/ design-system/ // tokens.css, shell, components storage.js // get(key) / set(key,val) — platform | localStorage backend.js // call(action, payload) -> Promise<json> nameplate.js // read(photos) -> { brand, model, serial, type, ageEst, confidence } version: "1.x" // pinned by each tool app

05The backend (stateless relay)

One Vercel serverless function, one endpoint, routed on an action field. It is stateless — no database, stores nothing. It exists purely to hold the keys and relay calls, which is why a single shared backend does not undermine front-end isolation.

Future option, not now. If a single function ever becomes a contention point, actions can be split into per-domain functions (e.g. /api/diagnose, /api/price) without any app change beyond the core backend client's routing. The stateless design makes that a later, low-risk move — full backend microservices are explicitly out of scope at this stage.

06Backend action contracts

The canonical request/response for each action. Earlier docs show fragments of these; this is the reference.

property Built

{ action:"property", address:"1420 Oak St, Brighton, CO" } // backend → RentCast GET /v1/properties?address= // returns public county / assessor records; tech can override { sqft:1840, yearBuilt:2006, stories:1 } // utility provider is NOT supplied by RentCast — resolved by the `utility` action below

utility Planned

{ action:"utility", address:"1420 Oak St, Brighton, CO 80601" } // backend → Anthropic API with web search enabled (same pattern as `rebates`) // prompt asks for the DISTRIBUTION utility (whose efficiency rebates apply), // not the retail energy supplier the customer may have chosen. { electric:{ name:"Xcel Energy (Public Service Co. of Colorado)", confidence:"high", sourceUrl:"…", alternatives:["United Power","IREA","Black Hills Energy"] }, gas:{ name:"Xcel Energy", confidence:"high", sourceUrl:"…", alternatives:["Atmos Energy","Colorado Natural Gas"] } } // alternatives = short curated list of other plausible providers for the area; // either name may be null with confidence:"low" if not found.

Mandatory confirmation gate (app-enforced). The utility result is a suggestion, never an authoritative value. When it returns, the Sales app pre-fills the electric and gas fields and opens a blocking confirmation window — "We found these utilities for this address. Is this correct?" — showing each provider, its confidence badge, and a source link. The salesperson must Confirm before the Customer & Property step can advance; the Next control stays disabled until then. Edit is a pick-from-list, not free text: tapping edit shows the returned alternatives for that area plus an "Other…" entry, so the chosen provider stays on a known, normalized name (clean data for rebates and the savings profile). What was detected, what was finally chosen, and who confirmed it are stamped to the survey. Low-confidence or null results still require an explicit confirm — the gate never auto-passes. Confirming first sharpens the downstream rebates and Savings Profile (§08), both of which read the utility and its rate.

nameplate Built

{ action:"nameplate", images:[ {base64, mediaType} ] } // backend → Anthropic vision { brand, model, serial, type, tonnage, ageEstimate:{ years:10, confidence:"high", flagged:true } } // age is best-effort, brand-specific, flagged for verification

rebates Built

{ action:"rebates", address, utility, equipmentTypes:[…] } // backend → Anthropic API with web search enabled { programs:[ { name, level:"federal|state|utility", amount, sourceUrl } ] } // live snapshot; amounts always verified by the tech

parsebill Planned

{ action:"parsebill", images:[ {base64, mediaType} ] } // 1+ utility bills // backend → Anthropic vision (same pattern as `nameplate` / `parseprices`) // extract USAGE & COST ONLY — never the account number, name, or address. { bills:[ { fuel:"electric", periodMonth:"2026-01", usage:1180, unit:"kWh", cost:154.20 }, { fuel:"propane", periodMonth:"2026-01", usage:92, unit:"gal", cost:285.20 } ] }

Optional, never required — and privacy-bounded (Safety condition). Bill upload is an opt-in accuracy upgrade for the Savings Profile, offered as "want an exact number instead of an estimate? add a recent bill or two." It never gates the survey. The default ask is one peak-summer + one peak-winter bill (most of the seasonal accuracy for minimal burden); 12 months is ideal when a serious buyer will provide it, including by email after the visit. A utility bill is sensitive personal data, so the action extracts usage and cost only — the account number, name, and any occupant detail are not extracted, and the bill image is never persisted (held in memory for the one read, like full-res nameplate photos). Parsed values override the energy-profile defaults (§07) and flip the Savings Profile (§08) from "estimate" to "based on your bills."

rates Planned

{ action:"rates", utility:{electric, gas}, state:"CO", region:"rocky_mountain" } // backend → EIA open data API (free, structured), cached & refreshed ~monthly. // fallback: Anthropic + web search for a utility's current headline rate. { electric:{ pricePerKwh:0.141, level:"utility", // utility-level when known, else state source:"EIA-861", asOf:"2024", trendPctPerYear:5.2 }, gas:{ pricePerTherm:0.98, level:"state", source:"EIA", asOf:"2026-02", trendPctPerYear:6.0 }, propane:{ pricePerGal:3.05, level:"region", source:"EIA SHOPP", asOf:"2026-02", trendPctPerYear:4.0 } } // asOf = the vintage of the figure; trendPctPerYear = EIA's recent YoY change, for the escalation note.

Every rate carries its vintage (Safety / honesty condition). EIA is a proxy for the effective residential price (revenue ÷ sales), which already blends fixed charges and riders — good for a savings estimate — but it lags: utility-level figures run ~a year behind, state/region figures ~2 months. So each rate returns an asOf date and a recent trendPctPerYear. The Savings Profile (§08) must show the as-of date with an asterisk on every Basis-tier figure and state the escalation context, so a salesperson can say "these use 2024 rates; prices are up ~10% since, so your real savings are likely higher." It is never silently presented as current.

estimate Planned (prototype: AI estimate)

{ action:"estimate", system, tier, property:{ sqft, yearBuilt, region }, // region → altitude / climate site:{ access, equipmentLocation, ductwork, lineSet:"reuse|new|unknown" }, existing:{ fuel, refrigerant, ageYears }, // drives tear-out / hazmat special:[ "finished_basement", "crawlspace", "attic" ] } // backend → Anthropic + web search (PROTOTYPE). Later: replaced by Pricing-tool import (hooks). { allInPrice:12800, range:{low:11000, high:15000}, breakdown:{ equipment, labor, materials, lineSet, tearOutDisposal, hazardous, permit }, assumptions:[…], asOf:"2026-06", confidence:"rough" }

Prototype pricing is a single all-in estimate (Safety: clearly an estimate). Until the Pricing tool is wired, this action returns one complete-package figure per system/tier — equipment, labor, materials, consumables, line set (reuse vs. new), tear-out & disposal, hazardous handling (old-refrigerant recovery; possible asbestos in pre-1980 homes), and permit — researched on the web and labeled a rough estimate with a range. It bakes in Colorado specifics (altitude sizing, ultra-low-NOx 96%+ AFUE furnaces). When pricing import is built, the same call is served from the Pricing tool's price book (cost) + company labor + markup; the contract stays the same so the hook is a drop-in.

diagnose First pass

{ action:"diagnose", equipment, jobType, complaint:{ reported, confirmed }, components:[ {name, brand, model, serial} ], trail:[ {step, value, unit} ], // actual readings so far tech:{ level, epaType, gates, conditionalOverride }, images:[ {base64, mediaType} ] } // optional step photos // returns exactly one of: { kind:"step", step:{ stepId,title,how,expects:{type,unit},inlineCaution,requiresGate } } { kind:"diagnosis", diagnosis, repair:{ requiresGate, steps:[…], inlineCaution } } { kind:"hardstop", hardstop:{ stopId,type,message,action,clearedBy:{gates:[…]} } }

The model proposes; the app decides. The diagnose response is advisory. The Repair app authoritatively evaluates requiresGate against the tech's profile and enforces every hard stop (§09–§10). The model is instructed to escalate on low confidence rather than fabricate a next step.

parseprices First pass

{ action:"parseprices", images:[ {base64, mediaType} ] } // invoice / screenshot // backend → Anthropic vision; extracts line items for the price book { items:[ { description, sku, vendorCost, vendor } ] }

ping Built

{ action:"ping" } → { ok:true } // the Test button's health check

07Data models

All persistence is per-device today. Each tool owns its own keys, so models stay decoupled across the split.

ModelStorage keyOwner app
Surveys (customer, property, systems, site, needs, goals, energy profile, site photos, rebates, proposal)fieldeng_surveys_v2Sales
Tech profile (admin-set)fieldeng_tech_profileRepair
Trouble calls (complaint, components, trail, diagnosis, verify, stamps, escalation)fieldeng_tech_calls_v1Repair
Price book + settings (cost, markup rules, view toggle)fieldeng_pricebookPricing
Inventory + locations (parts, per-location quantities)fieldeng_inventoryInventory
Backend URL + optional tokenfieldeng_backend_url / _tokenCore (all)

Site documentation photos (Sales) Planned. Beyond equipment nameplates, the survey captures a labeled photo set for the proposal and the future install crew: install location, home exterior, interior equipment area, electrical panel, nameplates, and access/obstructions — add as many as needed per category. Thumbnails persist on the record (same handling as nameplate photos); they ride along on the proposal and the office handoff.

Energy profile (Sales survey) Planned

// captured during the survey; feeds the Savings Profile (§08). Unknowns → regional defaults. { heatingFuel:"propane", // natural_gas | electric_resistance | heat_pump | propane | oil propane:{ tank:"leased", // owned | leased (null if not propane) tankGal:500, pricePerGal:3.10, annualGallons:480, estimated:true }, // true when price/usage came from defaults, not the customer solar:{ present:true, sizeKwDc:7.2, netMetered:true, ageYears:3 }, battery:{ present:true, usableKwh:13.5, backs:"essentials" }, // whole_home | essentials | partial bills:{ provided:false, months:[] } // optional; populated by `parsebill` (§06). } // when bills.provided, parsed usage/cost override the // estimated fields above and the Savings Profile uses actuals.

Tech profile (canonical) First pass

// admin-set only; tech cannot edit own credentials { techId:"T-1042", name:"Johnny R.", level:"novice", // novice | mid | senior | master epaType:"II", // "II" | "Universal" epaCertNumber:"608-XXXXX", gates:["E1","HP"], // admin-granted skill gates credentialNumbers:{ E1:"…", HP:"…" }, conditionalOverride:false }

Job record (canonical) First pass

{ jobId, techId, timestamp, equipment, complaint:{ reported, techConfirmed }, components:[ /* nameplates + serials + thumbnails */ ], diagnosticTrail:[ { stepId, value, unit, time } ], diagnosis, repairGuided, verify:{ reSymptom, reading }, stamps:{ techId, epaCertNumber, credentialsUsed:[…], acknowledgments:[…] }, escalation:null // or { reason, atStep, preservedTrail } }

08Recommendation engine (Sales) First pass

Runs entirely on the device — no internet call — so the recommendation works with no signal. Lives inside the Sales app.

Sizing (heuristic, not a Manual J)

Cooling tons = (sq ft ÷ 500) × 1.15 if built before 1980 (leakier) × 0.90 if built after 2010 (tighter) rounded to nearest 0.5, limited to 1.5–5 tons Heating band = sq ft × 35 to sq ft × 45 BTU (cold-climate assumption, rounded to 1,000)

Good / Better / Best tiers (fixed reference table)

System typeGoodBetterBest
AC / cooling14.3 SEER2 single-stage16 SEER2 two-stage18+ SEER2 inverter
Furnace / boiler80% AFUE96% AFUE98% AFUE modulating
Heat pump / mini-split15 SEER2 / 8.1 HSPF217 SEER2 / 9 HSPF219+ SEER2 / 10+ HSPF2 cold-climate

Flags

Per-system: equipment age 15+ (end-of-life) or 12–14 (aging); condition Failed; gas furnace → heat-pump conversion candidate. Whole-home: heat-pump rec + small panel (100A or 125A, or no open breaker spaces) → panel-upgrade flag; Poor/Fair ductwork and line-set marked Replace → scope items; humidity/dust → indoor-air-quality opportunity; hot-cold/uneven → zoning opportunity; maintenance-agreement prompt always appears.

Sizing is a check, not an authority. The heuristic estimate is used to cross-check the installed equipment: when the estimate and the existing tonnage disagree (by ~1+ ton), or confidence is low (square footage missing/suspect, conflicting inputs), the survey raises an "office to verify sizing" flag rather than presenting a confident number. It confirms a sane match or routes the questionable ones to the office — it never stands in for a proper load calculation.

Electrical panel field. Captured in Site Conditions as one of 100A · 125A · 150A · 200A · 200A+, plus an open-breaker-spaces check. The panel-upgrade flag fires for an electrification (heat-pump) recommendation when the panel is 100A or 125A, or when there are no open breaker spaces; 150A and up generally clear unless spaces are full.

Savings Profile (estimate) Planned

A directional estimate of annual energy cost, current setup vs. each proposed tier — the value story for the close. Computed on-device (offline) from the energy profile (§07), the sizing estimate above, and the confirmed utility rate. It is an estimate, never a guarantee.

annual_heating_energy ≈ heating_band(BTU) × climate_run_hours // from sizing current_cost = annual_energy ÷ current_fuel_efficiency × fuel_rate fuel_rate from `rates` action (§06): electric $/kWh · gas $/therm · propane $/gal · oil $/gal — each tagged with asOf + source bills (§07) override fuel_rate with the customer's actual $ when provided proposed_cost = annual_energy ÷ proposed_efficiency(SEER2/AFUE/HSPF2) × electric_or_gas_rate solar_offset = min(annual PV production, new electric load) // if solar.present savings_range = (baseline_cost − proposed_cost − solar_offset value) ± confidence band simple_payback ≈ net_install_cost ÷ annual_savings // rough, when price known // baseline_cost = annual cost of the SELECTED comparison baseline (dropdown, below)

Comparison baseline (selectable)

The salesperson picks what the proposed system is measured against from a dropdown, so the screen always reads "estimated savings vs. [baseline]." Options appear only when they apply:

Baseline optionProposed system compared vs.Available when
Your current system (do nothing)Keeping the existing unit running as-isAlways — the default
Repairing your current systemThe repair estimate + running the aging unit onA repair estimate exists — the Repair-vs-Replace handoff, or one is entered
A basic replacement (Good tier)The entry-level new systemProposed tier is Better or Best (shows the upgrade's payback)

One engine, two tools. This is the same calculation the Repair-vs-Replace Advisor (Stage 2 §B8) uses — its "replace" column is simply this panel with the Repairing your current system baseline selected. The proposed tier is switchable too; changing either side recomputes the range at its confidence tier. Honest framing (Safety): the baseline is always stated in words, every option uses real figures, and no baseline is hidden to inflate the number.

Estimate, not a guarantee (Safety condition). Every figure is shown as a range with its assumptions visible (fuel rate, usage, efficiency, solar offset), labeled an estimate, and never called "guaranteed" — the same discipline as the sizing heuristic. Even the top tier projects future savings, so it stays an estimate; more data only narrows the band. The salesperson can adjust any assumption, and figures are tech-verifiable before the customer sees them.

Rate vintage & escalation (the asterisk)

Because the rates data lags (§06), the Basis tier must be honest about when the rate is from. On the recommendation/presentation screen, every Basis-tier dollar figure carries an asterisk tied to a footnote stating the source and the as-of date — and the gap to today:

* Estimate. Based on {utility} {level} energy rates from {source}, {asOf}. Energy prices have risen ≈{trend × yearsSince}% since then, so your actual rates — and savings — are likely higher. Add a recent bill for an exact figure.

So a 2024 rate viewed in 2026 reads, e.g., "* Based on 2024 EIA rates; prices are up ~10% since, so real savings are likely higher." Two deliberate choices: the headline number is computed on the as-of rate (the conservative figure — we don't silently inflate the rate to bump savings), and the escalation appears as a stated, EIA-trend-derived note so the salesperson can speak to it honestly. Framed this way the vintage is a strength — the estimate is conservative, and rising prices make the case better, not worse. When the customer supplies bills, the figure switches to actuals and the asterisk goes away.

Headline cases the MVP targets first: propane → heat pump (large, defensible delta) and solar-offset electrification (PV covers much of a new heat pump's load). Battery presence informs the resilience/outage talking point and may affect incentive eligibility, but is not part of the cost math in the MVP.

Confidence tiers

The number the customer sees comes in three levels — the more real billing data they share, the tighter the band. The level is set by how many months of bills parsebill (§06) returns, and the label on screen states it plainly.

TierInputsHow it's computedLabel shown
Basis
rough
No bills — regional defaults + sizing heuristic + confirmed utility rateModeled usage from square footage and fuel; widest range"Rough estimate"
Better1–2 bills (default ask: one peak-cooling + one peak-heating month)Anchors actual usage/cost at the seasonal extremes, models the months between; mid range"Based on sample bills"
Best12 months of billsFull actual annual usage and cost; tightest range"Based on a full year of bills"
tier = bills.months.length >= 12 ? "best" : bills.months.length >= 1 ? "better" // 1–11 months, peak pair preferred : "basis" // tier sets the confidence band width and the on-screen label; never a guarantee at any tier.

Always available, never required. Basis needs nothing from the homeowner and works offline, so a number is always there. Better and Best are opt-in upgrades via parsebill — bills can be added on-site or emailed after the visit, and the survey never blocks waiting for them. Privacy stays bounded per the parsebill rules (usage/cost only, image never persisted).

Customer goals → recommendation framing Planned

Captured on the Comfort & Needs step as a multi-select with one marked primary. Goals don't change the engineering (sizing, tiers, flags are unchanged) — they decide which tier the screen leads with and which story gets top billing, so the same survey lands differently for a price-driven buyer than a comfort-driven one.

GoalRecommendation leads with
Lowest upfront costGood tier + financing; cheapest path that solves it
Best overall valueBetter tier; lifetime cost + the Savings Profile
Lower energy billsSavings Profile front and center; efficiency deltas
Improved comfortComfort flags (zoning, IAQ, two-stage/variable), not price
Reliability / peace of mindEnd-of-life flags, reliability, maintenance plan
Environmental impactHeat-pump / electrification + emissions story + rebates
Direct replacementLike-for-like match; minimal change & disruption
Best availableBest tier; top performance, budget not the constraint
goals: ["lower_bills","comfort"] // multi-select primaryGoal: "lower_bills" // exactly one; drives which tier + story leads // "More" also offers: staying_long_term · selling_soon (context for framing)

Honest framing, not steering. Goals reorder emphasis, never hide options — all tiers and the real numbers stay visible. A price-driven customer still sees the value case; a value-driven one still sees the cheap option. This keeps the "Quality Without an Upsell" brand intact while meeting the customer where they are.

Pricing the job Planned (prototype: AI estimate)

The quote auto-builds from the chosen tier — each tier is assembled from components at quote time (slower than fixed packages, but more accurate). It estimates the parts the job actually needs, not a generic list:

It asks about access & special conditions that move the price — a new line set fished through a finished basement, work in a crawlspace or attic, etc. — pre-filling what Site Conditions already captured (access easy/moderate/tight) and asking only for what's missing.

For the prototype, the estimate action (§06) returns this as a single all-in package price (with a range), web-researched and labeled rough — e.g. a complete furnace + AC for a ~2,000 sq ft Front Range home estimates around $11,000–$15,000 all-in. When pricing import is built, the same call is served from the Pricing tool: price-book cost + company-entered labor + company-entered markup → sell price, with cost/margin hidden from the salesperson per §11.

Adjustments & scope add-ons

Flagged scope items are preset, priced add-ons the rep can toggle on — panel upgrade, ductwork replacement, new line set, IAQ, zoning. Beyond the presets, an "Other" entry lets the rep describe a need in their own words; the tool prices it from company labor rate + company markup + AI-estimated materials (all AI-estimated in the prototype; labor and markup come from config once entered).

Proposal & close Planned

The survey now ends in a customer-facing proposal that closes the deal, not a text summary. It does four things: present the proposal, show financing, capture acceptance, and hand off the scheduled job.

proposal: { tier, systems:[…], sellPrice, // what the customer pays downPayment, // required deposit balance, dueOn, // remainder + when it's due financing: [ // up to 2 options, entered per deal { lender, termYears, apr, monthlyPayment }, // monthlyPayment = estimated { lender, termYears, apr, monthlyPayment } ], financingDisclaimer: "Estimated — subject to credit approval", // always shown rebatesEstimated, // from `rebates`, verified netOutOfPocket, // sellPrice − rebatesEstimated ← headline deposit: { pct:60, amount, dueAt:"acceptance" }, // office-configurable balance: { pct:40, amount, dueAt:"completion" }, signature, acceptedAt, status: "ready_to_install", // office then confirms equipment + schedules handoff: { googleCalendar:false, quickBooks:false } // set true once office backend syncs }

Pricing visibility & commission (roles). The customer and salesperson see the sell price and the out-of-pocket total. The salesperson may discount only down to an enforced minimum (a floor that protects margin) and never sees the cost or the margin. Commission scales with the sell price — closing higher earns more; discounting toward the floor (never below it) earns less. Only the sales manager and office see the exact system cost and margin. This requires the roles/accounts model (deferred — §14); until logins exist the app can enforce the floor and hide cost, but true protection arrives with accounts. Live prices come from the Pricing tool once wired (Stage 3).

Office handoff & integrations Planned. The accepted job pushes to the office backend, which integrates with a Google job calendar (the install slot once the office confirms) and QuickBooks (the 60% deposit invoice, 40% on completion). Actual payment is handled in QuickBooks/office, not the field app. The Google and QuickBooks OAuth credentials live server-side in the office backend, never in the field app — same principle as the API keys (§11). Deposit split and the lender list are office-configurable.

Proposal document layout Planned

The customer-facing PDF, in order — Empower-branded (charcoal header, cobalt accent, orange action), single page:

  1. Header — Empower wordmark + "Quality without an upsell," proposal #, date, valid-through date.
  2. Customer & property, prepared-by rep.
  3. Recommended system — the goal-driven primary tier, highlighted, with key specs and a one-line why.
  4. Good / Better / Best — three columns side by side, recommended one outlined, each with spec + price (lets the customer trade up/down).
  5. What's included — equipment, install, removal & disposal, line set, permit, startup/testing, warranty, rebate paperwork.
  6. Your investment — system price, estimated rebates (−), net out-of-pocket (headline), deposit (60%) at acceptance, balance (40%) at completion.
  7. Estimated savings — annual figure vs. the selected baseline, carrying the dated-rate asterisk.
  8. Financing — the two entered options (monthly / term / APR), "subject to credit approval."
  9. Rebates & incentives — programs with level + estimated amount, "amounts subject to verification."
  10. Acceptance — signature + date; signing accepts and authorizes scheduling.
  11. Fine print — estimates not guarantees; savings on dated rates; financing subject to approval; rebates to verify; sizing preliminary/confirmed before install; proposal valid 30 days (configurable).
  12. Footer — contact, license #.

Never on the customer document: cost, margin, or commission. The proposal shows sell price, rebates, net out-of-pocket, deposit/balance, savings, and financing only — the internal figures stay in the rep's private screen and the manager/office views (§11). Optional second page: site photos + detailed scope/terms.

09Credential model & gate evaluation (Repair) First pass

Two layers plus an override flag, all admin-set. Gates govern what work is permitted; level governs how much guidance and override eligibility; the override flag clears specific conditional stops. Evaluated authoritatively in the Repair app.

CodeGateUnlocks (beyond baseline)
EPA-UEPA 608 UniversalLow-pressure / chiller (Type II high-pressure work is baseline)
E1Line-voltage (single-phase)Burned line-voltage wiring, whips, disconnects, hard-wired motor rewiring
E2Three-phase (hands-on)Hands-on 3-phase work (measurement on 3-phase is allowed for all)
E3New circuit / branchNew circuit from panel, branch extension, new disconnects
G1Gas valve / pipingReplace gas valve, broken-seal gas connections (leak-test required)
G2Combustion & ventingCombustion analysis (CO/O₂), venting sign-off, confirming a cracked HX
HPHeat pumpsReversing valve, defrost, low-ambient, aux/emergency-heat, HP charging
B1Hydronic boilersCirculators, expansion tanks, zone valves, aquastats
B2Steam boilersSteam controls, LWCO, pressuretrols, sight glass (full stop without B2)
WHWater heatersGas control valve, T&P, thermocouple/igniter, tankless flow & descaling
CBCombi unitsCombi boiler + DHW combined systems
OILOil-firedDefined but dormant until oil service is offered

Gate principle. Measurement & diagnosis are never gated (3-phase included) — only hands-on modification carries a gate. Combustion-dependent work on any fuel-burning unit also requires G2. Gates stack (a 3-phase HP contactor swap needs E2; a boiler combustion tune needs B1+G2). Gates are admin-set; a tech cannot self-grant.

// Repair app, on every AI result — before guiding any repair or clearing any stop function canPerform(action, tech) { if (action.requiresGate == null) return ALLOW; // baseline / measurement if (action.requiresGate == "EPA2") return ALLOW; // all techs hold Type II if (tech.gates.includes(action.requiresGate)) return ALLOW; return BLOCK_ESCALATE; // preserve work-so-far, route to escalation } function canClearConditional(stop, tech) { return tech.conditionalOverride && stop.clearedBy.gates.every(g => tech.gates.includes(g)); }

10Safety enforcement in app logic First pass

Three mechanisms, all enforced in the Repair app — never left to the model.

TriggerTypeBehavior
Gas odor / suspected leakUniversalStop, ventilate, shut gas, escalate — no override
Active CO alarmUniversalStop, ventilate, shut down appliance, escalate
Standing water + energized equipmentUniversalKill power safely if possible, stop, escalate
Suspected cracked heat exchangerConditionalRed-tag + escalate for baseline/mid; G2 + override may proceed after acknowledgment
Readings don't reconcile / low confidenceConditionalStop and escalate rather than guess; senior/master with override may continue

Universal stops apply to everyone regardless of credential. Conditional stops halt baseline/mid techs, but a tech with the matching gate and the conditionalOverride flag may proceed after an on-screen acknowledgment that is stamped to the job with the Tech ID.

The Repair-vs-Replace Advisor is subordinate to safety. The advisor (Stage 2 §B8) runs only after any hard stop has been actioned and never overrides one. A cracked heat exchanger is red-tagged and the unit shut down whether or not a replacement is sold; the replace conversation happens after the safety action, never instead of it.

11Security & privacy

Roles & accounts Planned

Three roles, office-assigned. Sensitive values are filtered at the source for the role — in production the backend never sends cost/margin to a salesperson's session, so hiding isn't just a UI toggle. (In the on-device prototype, roles are set locally for testing and the app gates the view; true enforcement arrives with the central store + login.)

Can see / doSalespersonSales managerOffice / admin
Sell price & net out-of-pocket
Discount only to the minimum (margin floor)✓ enforced✓ sets it
Exact system cost & margin
Own commission & payout✓ (private screen)
Other reps' deals / payouts✓ team✓ all
Configure price book, financing, rebates, userspartial

12Storage & persistence

The core storage abstraction uses the platform store when present (Claude preview) and falls back to the browser's localStorage on the hosted site, so surveys and trouble calls persist on the device across sessions. Data is per-device and per-browser — not synced to a central server. Photo thumbnails (~200px) are persisted within each record; full-resolution images are held in memory only for the session and sent once for the AI read, never saved.

Data sharing (central store) Planned

For a manager to see a rep's deals, records must live beyond the device — designed to share without heavy transfer:

13Cross-tool integration (planned) Planned

With apps split, the planned Stage 3 links become explicit contracts rather than in-process calls. Each is a read across a boundary, mediated so isolation holds.

Design rule for links. A cross-tool read must never be a hard dependency that breaks the consumer — if Pricing is down, Sales still runs. This preserves the fault isolation the split exists to provide.

14Migration path & open decisions

From single file to split

Open decisions

15June 2026 additions — electrification & identification

These extend the existing module boundaries; none break the device-local, stateless-relay model. Reasons are recorded so future maintainers understand intent.

New backend action contracts (stateless relay)

ActionIn → OutNotes
parsebill{images[]}{serviceAddress, bills[]}Vision read of electric/gas bills (two utilities). Each bill: fuelType, provider, month, usage, unit, cost, pricePerUnit, peakKw, avgTempF, degreeDays. No web tool.
identify{images[]}{units[], systemSummary}Vision + web_search. Each unit carries a coarse unit kind (furnace/ac/heat_pump/rtu/boiler/evaporative_cooler/other) plus an additive finer subtype (heat_pump_outdoor/indoor, minisplit_outdoor/head, ac_condenser, evaporator_coil, rtu, zoning, water_heater, thermostat, line_set) + per-kind specs + ageEstimate. Evaluator assembles by unit (ignores subtype — backward-compatible); Sidekick maps subtype → component type. De-duplicates repeat photos.

Both follow the existing relay contract: no state stored server-side, images passed through to the model and discarded. nameplate (single unit) is retained for per-system reads.

Data-model deltas

Engine routing & gating (shared core)

Boundary note: all of the above lives in the Sales module and shared core; the customer-app security boundary and the Repair safety-enforcement rules are unchanged. The richer per-home dataset (bills, specs, multi-unit identities) is the same data Sidekick (Repair) will consume.

Field Engineer — Architecture & Design (software definition). Canonical reference for the modular-split build; supersedes the architecture fragments in the Technical Reference and Stage 2 spec. Update alongside future changes.

Customer-app security boundary (Concierge Stage-1)

The public support page (support.html) shares the one serverless backend but crosses a trust boundary. The backend now splits actions: a PUBLIC_ACTIONS set (triage, support_request) bypasses the APP_TOKEN gate so unauthenticated customers can use them, while every technician/office action still requires the token. Public actions enforce payload caps (description length, image count) and return only customer-safe content; the partsHint and any internal data are emailed to the shop, never returned to the public page in a way that exposes operations. triage performs emergency detection and high-level causes only (no repair steps); support_request sends via a transactional email API (Resend) keyed by server-side env vars the customer never sees, with a client-side mail/SMS/call fallback. Known limitation: an unauthenticated AI endpoint is cost-bearing and abusable — effective rate-limiting needs a stateful layer (edge WAF or KV) that the device-local model lacks; this is the first concrete driver for the office backend.