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.
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.
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.
/api/lookup) that holds API keys and relays to external services. Because it stores no per-tool state, one relay safely serves every app; isolation lives at the front end, not here.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 app | Owns | Depends on | Status |
|---|---|---|---|
| Sales / Survey | Survey flow, recommendation engine, rebate request UI | Core; backend property, utility, nameplate, rebates, parsebill, rates, estimate | First pass |
| Pricing | Price book, markup rules, owner/tech view toggle | Core; backend parseprices | First pass |
| Tech / Repair | Diagnostic loop, credential gating & hard-stop logic, job record | Core; backend diagnose, nameplate | First pass |
| Inventory | Locations, parts, per-location quantities, search | Core (local only in first pass) | First pass |
| Customer App | Public service request | Core (UI only); backend public actions | Planned |
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 module | Responsibility | Source today |
|---|---|---|
| Design system | Fonts (Archivo / IBM Plex), palette, component styles, the app shell chrome | Shared across all four current docs |
| Storage abstraction | Platform store when present (Claude preview), localStorage fallback on the hosted site; per-key get/set | Tech Ref §04 (resolved) |
| Backend client | Builds the action request, attaches the optional app token, posts to /api/lookup, returns clean JSON or a typed error | Tech Ref §01–02 |
| Nameplate read | Captures photos, downscales to thumbnail, sends full-res once for the AI read, returns structured fields + confidence | Tech Ref §03, §08 (shared by Sales & Repair) |
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.
POST /api/lookup, dispatching on action.property, utility, nameplate, rebates, parsebill, rates, estimate, diagnose, parseprices, ping (health check for the Test button).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.
The canonical request/response for each action. Earlier docs show fragments of these; this is the reference.
property Builtutility PlannedMandatory 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 Builtrebates Builtparsebill PlannedOptional, 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 PlannedEvery 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)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 passThe 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 passping BuiltAll persistence is per-device today. Each tool owns its own keys, so models stay decoupled across the split.
| Model | Storage key | Owner app |
|---|---|---|
| Surveys (customer, property, systems, site, needs, goals, energy profile, site photos, rebates, proposal) | fieldeng_surveys_v2 | Sales |
| Tech profile (admin-set) | fieldeng_tech_profile | Repair |
| Trouble calls (complaint, components, trail, diagnosis, verify, stamps, escalation) | fieldeng_tech_calls_v1 | Repair |
| Price book + settings (cost, markup rules, view toggle) | fieldeng_pricebook | Pricing |
| Inventory + locations (parts, per-location quantities) | fieldeng_inventory | Inventory |
| Backend URL + optional token | fieldeng_backend_url / _token | Core (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.
Runs entirely on the device — no internet call — so the recommendation works with no signal. Lives inside the Sales app.
| System type | Good | Better | Best |
|---|---|---|---|
| AC / cooling | 14.3 SEER2 single-stage | 16 SEER2 two-stage | 18+ SEER2 inverter |
| Furnace / boiler | 80% AFUE | 96% AFUE | 98% AFUE modulating |
| Heat pump / mini-split | 15 SEER2 / 8.1 HSPF2 | 17 SEER2 / 9 HSPF2 | 19+ SEER2 / 10+ HSPF2 cold-climate |
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.
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.
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 option | Proposed system compared vs. | Available when |
|---|---|---|
| Your current system (do nothing) | Keeping the existing unit running as-is | Always — the default |
| Repairing your current system | The repair estimate + running the aging unit on | A repair estimate exists — the Repair-vs-Replace handoff, or one is entered |
| A basic replacement (Good tier) | The entry-level new system | Proposed 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.
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:
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.
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.
| Tier | Inputs | How it's computed | Label shown |
|---|---|---|---|
| Basis rough | No bills — regional defaults + sizing heuristic + confirmed utility rate | Modeled usage from square footage and fuel; widest range | "Rough estimate" |
| Better | 1–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" |
| Best | 12 months of bills | Full actual annual usage and cost; tightest range | "Based on a full year of bills" |
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).
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.
| Goal | Recommendation leads with |
|---|---|
| Lowest upfront cost | Good tier + financing; cheapest path that solves it |
| Best overall value | Better tier; lifetime cost + the Savings Profile |
| Lower energy bills | Savings Profile front and center; efficiency deltas |
| Improved comfort | Comfort flags (zoning, IAQ, two-stage/variable), not price |
| Reliability / peace of mind | End-of-life flags, reliability, maintenance plan |
| Environmental impact | Heat-pump / electrification + emissions story + rebates |
| Direct replacement | Like-for-like match; minimal change & disruption |
| Best available | Best tier; top performance, budget not the constraint |
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.
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.
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).
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.
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.
The customer-facing PDF, in order — Empower-branded (charcoal header, cobalt accent, orange action), single page:
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.
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.
| Code | Gate | Unlocks (beyond baseline) |
|---|---|---|
| EPA-U | EPA 608 Universal | Low-pressure / chiller (Type II high-pressure work is baseline) |
| E1 | Line-voltage (single-phase) | Burned line-voltage wiring, whips, disconnects, hard-wired motor rewiring |
| E2 | Three-phase (hands-on) | Hands-on 3-phase work (measurement on 3-phase is allowed for all) |
| E3 | New circuit / branch | New circuit from panel, branch extension, new disconnects |
| G1 | Gas valve / piping | Replace gas valve, broken-seal gas connections (leak-test required) |
| G2 | Combustion & venting | Combustion analysis (CO/O₂), venting sign-off, confirming a cracked HX |
| HP | Heat pumps | Reversing valve, defrost, low-ambient, aux/emergency-heat, HP charging |
| B1 | Hydronic boilers | Circulators, expansion tanks, zone valves, aquastats |
| B2 | Steam boilers | Steam controls, LWCO, pressuretrols, sight glass (full stop without B2) |
| WH | Water heaters | Gas control valve, T&P, thermocouple/igniter, tankless flow & descaling |
| CB | Combi units | Combi boiler + DHW combined systems |
| OIL | Oil-fired | Defined 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.
Three mechanisms, all enforced in the Repair app — never left to the model.
| Trigger | Type | Behavior |
|---|---|---|
| Gas odor / suspected leak | Universal | Stop, ventilate, shut gas, escalate — no override |
| Active CO alarm | Universal | Stop, ventilate, shut down appliance, escalate |
| Standing water + energized equipment | Universal | Kill power safely if possible, stop, escalate |
| Suspected cracked heat exchanger | Conditional | Red-tag + escalate for baseline/mid; G2 + override may proceed after acknowledgment |
| Readings don't reconcile / low confidence | Conditional | Stop 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.
APP_TOKEN env var requires every request to send a matching token, blocking casual use of the endpoint.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 / do | Salesperson | Sales manager | Office / 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, users | ✗ | partial | ✓ |
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.
For a manager to see a rep's deals, records must live beyond the device — designed to share without heavy transfer:
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.
estimate action (AI/web, all-in). In production, the same call is served by the Pricing tool — price-book cost + company labor + markup → sell price — with cost/margin hidden from the salesperson (§11). The contract stays the same, so wiring Pricing is a drop-in; Sales degrades gracefully (shows the rec without prices) if pricing is unavailable.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.
These extend the existing module boundaries; none break the device-local, stateless-relay model. Reasons are recorded so future maintainers understand intent.
| Action | In → Out | Notes |
|---|---|---|
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.
utility.gasService ∈ {confirmed, none, unknown} — field-confirmed; never inferred from the map. Gates dual-fuel and the gas-service rebate.energy.cooling ∈ {central, window, swamp, none} — drives the added-AC value flag.energy.bills.months[] entries gain fuelType, provider, unit, pricePerUnit, peakKw, avgTempF (fuel-tagged, multi-utility).sys.ac = {brand, model, serial} for the AC condenser of a two-unit furnace_ac, plus sys.acPhotos, sys.typeAuto, and (ductless) sys.zones.ductless added across TYPE_NAMES/TYPE_OPTIONS/TIER_SPECS/TIER_COST_BASE/EQUIP_DEFAULTS; existing-equipment type evap added.type (13-item taxonomy) with per-type spec fields (COMPONENT_TYPES / COMP_SPEC); job-type templates pre-load typed components; a batch identify auto-types components via mapIdentifyToCompType().recommendedType() → ductless for electric baseboard, and for electric/propane/oil when ductwork = "none".ductlessTierCost(idx, zones); zones default ~1/500 sq ft, editable via App.setZones.rebatesFromTons(tonsByType, gasService) — no-gas swaps the cold-climate $/heating-ton line for a conservative electric-utility line; ductless tons count as cold-climate.computeGasCheck() infers gas presence from fuel-tagged bills + fuel (flag, not authority).effectiveFuelPrices() prefers the customer's parsed bill rates over office regional rates.assembleSystems(units) pairs furnace+AC into one system, appends rather than overwrites, and reuses applyUnitFields() (the shared field-mapping core split out of applyNameplate).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.
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.