Eave
Software for Home Inspectors
Eave is a solo SaaS startup I built end-to-end for home inspectors — the people who walk a house room by room before it changes hands and write up everything they find. It replaces a stack of disconnected tools (a report template in Word, a scheduling link, a separate e-signature service, a spreadsheet of clients) with one product that covers the whole job: turning a website visitor into a booked inspection, getting the agreement signed before arriving, building the report on-site, flagging issues automatically, and getting paid.
It's a full-stack project in the truest sense — a Nuxt/Vue web app, an offline-capable native Swift iOS app with push notifications, a SQLite database behind Drizzle ORM, two separate Stripe integrations (my subscriptions and my customers' client payments), a set of Claude-backed AI features, and deployment on Cloudflare, all designed and shipped by me alone. The sections below walk through the major feature areas and the engineering decisions behind each one.

Custom Templates & On-Site Data Entry
Every inspection starts from a template — either one of the curated samples (Standard Home Inspection, Radon Testing, Mold Assessment, Sewer Scope, and more) or a fully custom one built from scratch. Templates are made of sections, subsections, and fields, and each field can be one of eight types: text, number, yes/no, dropdown, multi-select, rating, date, or photo/video. Inspectors can drag-reorder everything, set default values, and even define conditional visibility so a field only shows up when a prior answer warrants it.

The hard part wasn't the builder UI, it was the schema. Eight field types each need different configuration (a number field needs a unit and min/max, a multi-select needs min/max selections, a photo field needs a max count) without turning the template model into a pile of nullable columns or forcing a new database migration every time I want to add a field type. I settled on a typed, discriminated config object per field, validated with Zod, stored as JSON — so the schema evolves in code, not in the database, and TypeScript catches mismatched field configs at compile time.
On-site, inspectors fill out fields directly on their phone or laptop with autosave firing about 1.5 seconds after the last keystroke (with Cmd+S available to force it), and they can even tweak a field on the fly mid-inspection — attach a description, require it, rename it — without leaving the report to go edit the template separately.

Keeping the edits made on-site
Those on-the-fly edits used to live and die with a single inspection. Now, when an inspector changed the template while filling it out, a Template step appears at the end of the job and lists every change on its own line — "Renamed Type to Roof Type", "Added the field Attic photos", "Reordered the subsections in Exterior" — each with its own checkbox. They keep the real improvements, skip the one-off notes for that house, and either apply them to the source template or save the set as a new one.
Under the hood this is a three-way merge, not a diff: the template snapshot the inspection started from, what the inspector actually filled out, and the source template as it stands today. The first pair says what changed; the second says whether applying it would clobber a later edit made by a teammate. Conflicts are shown with both versions and left unchecked. Every change is a narrow patch (a rename carries only the label), so three edits to one field can be applied independently. Changes the template already has are dropped from the list, which makes the step fix itself — apply half today and tomorrow only the other half is waiting. The apply endpoint recomputes the change list server-side and filters by the ids the client sent; a client-supplied payload would be an arbitrary write into someone's templates.
Full guide: Editing a template →AI Features (Claude + Structured Outputs)
Eave uses Claude in three places. Each one is aimed at the slowest step of a particular job, and each one hands back something the inspector reviews and edits instead of something that ships unseen.
Template import
The biggest thing standing between an inspector and switching software is the template they've spent years refining in Spectora or HomeGauge. So Eave imports it: drop in an export — CSV, Excel, PDF, plain text, or HTML — and it comes back as a fully editable Eave template with the original sections, subsections, and fields intact.

I split the pipeline into a deterministic ingest layer and a probabilistic transform layer. Ingest only cares about the container format — spreadsheets through xlsx, PDFs through unpdf — and produces raw text with a hard cap on size. Interpretation is Claude's job: the extracted text goes out with a source-specific system prompt (Spectora and HomeGauge each have their own comment taxonomy worth explaining to the model) and a Zod schema as a structured-output contract, so what comes back is validated into the same typed template schema the hand-built editor produces. Anything the model gets wrong is just a normal template the inspector can edit — the import is a head start, not a black box.
A later pass taught the prompt the difference between a neutral list ("materials present") and a deficiency checklist. Deficiency lists now come across as Issues fields, where each option carries a severity and client-facing wording taken from the old template's canned comments, so ticking one writes a finding straight into the report. Every section and subsection also gets an icon picked to match what's in it.
The interesting constraint was the runtime. Cloudflare Workers cancel background work shortly after a response is sent, and a large template can take minutes to transform — so this can't be a fire-and-forget job behind a polling endpoint. Instead the transform runs inside a held-open SSE request that streams progress back to the browser while it works, and the extracted text is persisted before the model is ever called, so a dropped connection or a failed transform can be retried with one click and no re-upload.
Describe a subsection, get it built
Building a subsection by hand means adding each field, choosing its type, typing its options, and wiring up conditions and warnings one at a time. A Generate with AI button next to Add field takes a plain-English prompt — "add fields for roof material, age, and overall condition with a defect checklist" — and writes the fields, their types and options, their conditional visibility, and (on templates) their warning and home-care triggers.

The important constraint is that it edits, not replaces: the current subsection is sent along with the prompt, so asking for three more fields on a subsection of ten brings the original ten back untouched. The same schema contract serves both the template editor and the public intake form. The intake form gets a narrower set of field types and no report triggers, because its answers never become findings. The result lands in the editor as an ordinary unsaved edit, so nothing is saved until the inspector hits save.
Drafted summaries
The report summary is the first thing a client reads, and it's written at the point in the job when the inspector least wants to write anything. Generate with AI on the Final Comments field drafts one from the findings and answers already in the inspection: overall condition first, then the most serious findings, then routine maintenance. It's grounded only in that inspection's data, so it can't invent a finding, and it asks before replacing anything already written. Every account gets three free uses of each AI feature before they become part of Pro.
Full guide: Importing a template →Automated Warnings & Home-Care Rules Engine
As an inspector answers fields, Eave evaluates a set of rules attached to the template and automatically generates warnings (grouped by severity: Safety, Health, Structural, Electrical, Fire, Maintenance, Info) and home-care tips (grouped by cadence: Monthly, Quarterly, Bi-Annual, Annual, Seasonal, As-Needed) without the inspector having to write any of that language themselves. They can still edit severity, title, message, estimated cost, or timeline on any triggered item, hide ones that don't apply, or add fully custom ones.

The interesting problem here is what happens when a rule changes after an inspector has already hand-edited a warning it triggered — say I fix a typo in a rule's default message, or the inspector changes their answer to the underlying field. Naively re-running the rule would silently overwrite their edit; naively skipping re-evaluation would leave stale data. I built what I call stale overrides: once an inspector manually edits a triggered item, that edit is tracked separately from the rule's output and takes precedence, but the system still flags when the underlying rule or answer has since diverged, so nothing gets lost silently. On inspections marked auditable, all of it locks at publish time along with the rest of the report.

Publishing, Delivery & Optional Auditability
Once an inspection is complete, the inspector previews the report as a client would see it, downloads a PDF, and publishes it — which emails clients a stable, shareable link that keeps working across republishes. Additional recipients like the buyer's agent get a view-only copy.

The biggest change here was a change in the product, not the code. Early Eave treated the published report as the legal artifact: it was signed, locked, and versioned so there was a paper trail of exactly what the client saw. In practice the thing inspectors actually need signed is the pre-inspection agreement, and that happens before the walk-through (see the next section). Once the legal step moved there, a locked, version-tracked report was mostly friction — an inspector who spotted a typo after publishing had to go through a whole ceremony to fix it. So version history was retired and locking became something the inspector opts into.
An inspection can be marked auditable at creation (or while finishing a draft), and that choice is immutable afterward — a paper trail you can switch off later isn't much of a paper trail. A published auditable inspection is frozen: every content mutation (fields, photos, warnings, summary, clients, fee) returns 423 Locked from one shared server-side check, and a single isInspectionLocked helper drives the web UI and is mirrored in the iOS app. To make a change, the inspector unpublishes, edits, and republishes, and clients are emailed that the report was updated. Everything else stays editable after publishing: an edit just marks the generated PDF stale, so the next download regenerates it instead of serving something that no longer matches the report.
Signature capture for the inspector's own signature supports both drawing (mouse, trackpad, or touch) and uploading an image, which gets resized and compressed client-side before it's stored. Publishing is gated behind a profile-completeness check (license number, business info, and a signature all need to be on file), and inspectors choose which of those profile fields — address, licenses, phone, website, years in business, total inspections — show on the report, with per-inspection overrides that lock along with an auditable report.

One small detail I'm fond of: every link Eave sends (report, payment, agreement, booking, repair request) unfurls into a branded preview card in iMessage, WhatsApp, or Slack instead of showing up as a bare URL that looks like phishing. The catch is that link previews aren't built on the recipient's phone. Apple's, Google's, and Meta's servers fetch the page to build them, so anything in the Open Graph tags has left my system and sits in a message thread anyone can glance at. The preview names the inspector's business and never the property address or client, which only appear once the page is actually opened. The same links carry an explicit noindex, so a report posted somewhere public still can't show up in search results.
Pre-Inspection Agreements & E-Signing
Inspectors are supposed to have a signed agreement before they ever walk the property, and most of them chase it over email. In Eave they write the agreement once, attach it to an inspection when they create it, and every client with an email address automatically gets a signing link — so it comes back signed before the inspector pulls into the driveway. The client types their legal name, checks an "I agree" box, signs, and a signed PDF is stored on the inspection. This is the document that carries the legal weight now, which is what made report locking optional.

The design decision I care about here is that agreements are snapshotted, not referenced. An agreement template is copied onto the inspection at the moment it's attached, so later edits to the template can never retroactively change the document a client already signed — the same reason you don't render a legal document from mutable state. The signed copy is what the inspector can point to a year later if a claim comes up.

An unsigned agreement also locks the published report: anyone opening the report link gets a "review & sign" screen instead of the findings, and it unlocks the instant someone signs. Since payment can gate the report too, the two locks are resolved as an explicit, ordered chain — sign, then pay, then read — so a client never hits two walls in a row without knowing which one they're on.
Importing an existing agreement
Every inspector already has an agreement, usually a PDF their attorney signed off on, and nobody wants to retype it. Import from PDF turns one into an editable template. It strips the running headers, footers, and page numbers, rejoins words hyphenated across line breaks, works out headings and bullet lists, and converts fill-in spots — [Client Name], <<Inspector Name>>, Inspection Fee: ______ — into Eave merge fields.
This one is deliberately not AI. A legal document is exactly where a model rephrasing a clause "helpfully" is unacceptable, and the job is structural recovery rather than interpretation. So it's a mechanical, deterministic pipeline: fast, free, and it never changes a word it wasn't sure about. The result opens in the editor with a banner listing every substitution and everything it couldn't match, and nothing is saved until the inspector reviews it.
The editor itself renders merge fields as atomic, labeled chips ("Property address" rather than {{property_address}}) that insert at the cursor and delete with one keystroke. They serialize back to the exact same tokens on save, so every existing template picked up the new editor with nothing to migrate and no change to what clients sign.
Client Payments (Stripe Connect)
Inspectors collect their fee through Eave: the client gets a pay link (manually, or emailed automatically on creation or publish, with optional reminders) and pays by card, Apple Pay, Google Pay, or ACH bank transfer. The inspection marks itself paid, the inspector is notified, and the client gets a receipt. Inspectors can also require payment before the report unlocks.

This is built on Stripe Connect Express with direct charges — each inspector onboards their own connected account and the charge is created on it, so the money never touches a platform balance and Stripe handles KYC, payouts, refunds, and disputes. That's a deliberate positioning choice as much as an architectural one: competitors take 3–4% of every inspection fee, while Eave's platform fee is wired up and set to zero. It's also a second, completely separate Stripe integration from the SaaS subscription — its own webhook endpoint and signing secret — and the one security invariant that matters is that Checkout metadata is attacker-controllable, so every payment event verifies the connected account that sent it actually owns the inspection it names.

ACH made this a real state machine rather than a boolean. A bank transfer completes checkout without the money existing yet, so a payment moves through processing → paid or failed over several days, asynchronously, on webhooks that can arrive out of order, duplicated, or race the browser redirect. I kept a single timestamp as the source of truth for "actually paid", made Stripe events refuse to overwrite a payment the inspector recorded by hand, scoped idempotency to the Checkout Session so a re-collection after a refund still works, and pulled all of it into pure functions with unit tests — which matters more than usual on Cloudflare D1, where there are no interactive transactions to hide behind. The report gate deliberately fails open: if the inspector's account is restricted or the fee is zero, the report unlocks rather than trapping a client behind a payment they can't make.
Full guide: Payments overview →Built-in Client Scheduling
Rather than sending clients to a third-party Calendly-style tool, Eave has its own public booking page — no login required. Clients pick a day from a calendar that highlights open slots, pick a time grouped into Morning/Afternoon, and enter their contact details. Inspectors configure their own working hours per weekday, inspection duration, buffer time between jobs, and slot granularity, and org admins can set org-wide defaults that individual members inherit until they customize their own.

I modeled this as a request → confirm flow rather than instant-booking: a client submitting a time doesn't touch the inspector's calendar until the inspector explicitly confirms it. That avoids a whole class of double-booking bugs you'd otherwise have to solve with locking, since nothing is committed until a human approves it. It still needs real-time conflict protection, though, so a pending request holds its slot and the server recomputes open slots on every submission, rejecting a pick that no longer fits. Inspectors who find the confirm step pointless can now switch it off, and the client's pick is booked immediately. That applies only to booking links sent for a job the inspector already created. Requests from the public link (below) always wait, because anyone can open that one.
The booking page also stopped dead-ending. A client who is relocating, at work, or simply doesn't care which morning it is can choose "I'm flexible — you pick a time", which is also the only option shown when there's no open availability at all, so the page never falls back to phone tag.

A public link that turns visitors into inspections
Everything above assumes the inspector already knows who the client is. The public request link covers the person who finds their website at 9pm: one permanent link for a website, email signature, or the back of a business card (with a downloadable QR code for yard signs). Visitors describe the property, answer the inspector's intake questions, optionally pick a real open slot, and leave their contact details.

The design choice that kept this small was adding no new inspection columns. A submission creates a draft inspection that already has a pending schedule request attached, so the existing confirm/decline endpoints, their emails, and the double-book guard all work on it unchanged. The intake questions reuse the template field editor, conditional logic included, and each answer is stored together with the question as it read when submitted, so editing the form later never rewrites a lead that already arrived. The token survives turning the feature off and on — it's printed on business cards — and rotating it is a separate, deliberate action. Because this is the only public endpoint that creates rows, it's limited to subscribers (otherwise it would be a way around the free-tier cap) and stops accepting requests once ten unactioned drafts are waiting.
Scheduling with the whole day in view
Clients only ever saw slots that fit around other jobs, time off, and drive-time buffers, but the inspector booking a job manually got a bare date picker. Now setting a date draws the day to scale: other inspections with their buffers hatched in, time off, recurring blocks, working hours, and outlined gaps big enough for a full inspection. Click a gap to drop the job in, or use a shortcut like "Right after 1420 Birch Ct". When a time doesn't fit, a line underneath says exactly why — overlaps your lunch, leaves less than 30 min drive time — and offers the nearest time that does.

It's the same slot arithmetic the booking page runs, but with the opposite policy. For clients a conflict is a wall; for the inspector it's a warning. The save button turns amber and still saves, because double-booking a block or working a Saturday is a decision, not a mistake. Times are anchored to the timezone in the inspector's scheduling settings rather than their laptop's clock. Saving a time also offers to email every attendee, and if the job is rescheduled after that, the inspection flags that attendees are "holding the wrong time" until they're told.
All of this now lives on one Scheduling page whose default tab is a month calendar. Every job is colored by what it needs from the inspector: scheduled, waiting on their confirmation, or still a draft. A lead from the public link is both a draft and a pending request, and it shows as pending, because the thing you can act on today outranks the thing you can't. The grid and the day timeline under it are built from the same per-day agenda function, so a day can never look different in the two views. Requests where the client asked the inspector to pick the time have no date to sit on, so they get a strip above the grid instead of getting lost.
Full guide: Scheduling overview →"Needs Your Attention" Dashboard
The dashboard was good at showing what's coming up and what the inspector earned, but bad at showing what they owe someone. That information existed, just scattered one inspection at a time. A booking request only showed up if you opened that job, and a report PDF that failed to generate showed up nowhere. Now a Needs your attention table catches ten kinds of open item across four groups: clients waiting (requests to confirm, drafts to finish), report work (overdue reports, failed PDFs), money and paperwork (declined payments, unpaid reports, unsigned agreements), and scheduling (no date set, attendees holding a stale time).

The design rule is one row per job, not one per problem. Each inspection shows only its single most urgent item and links straight to the stage that resolves it, so the list stays a worklist instead of turning into a wall. What it leaves out matters as much. An unsigned agreement three weeks out is normal and stays quiet until the job is close, a booking link sitting with the client is waiting on them, and when nothing is open the table doesn't render at all.
Repair Requests
This is a feature unique to Eave that came directly from talking to inspectors and buyer's agents: after a report is published, a buyer's agent (or anyone with the report link) can build a free "repair request" — the ask list sent to a seller — directly from the inspector's findings, instead of re-typing everything into a separate Word doc. They check off findings, choose a request type per item (Repair, Replace, Credit, Further evaluation, Other), add credit amounts or notes, and include custom items not in the report at all.

The builder reads live off the published report, which creates a reconciliation problem: what happens when a finding the buyer already referenced gets edited or removed from the report after the fact? Deleting the buyer's text outright would be destructive and confusing. Instead, a removed finding is flagged as "no longer in the report" in the repair request UI while the buyer's existing wording is preserved — the two documents can drift without either one silently corrupting the other.

Mobile Web & Native iOS App (Swift)
Inspectors spend most of their working hours walking through houses, not sitting at a desk, so the mobile experience isn't an afterthought. The mobile web app works fully in the browser — camera capture with automatic photo resizing and HEIC-to-JPEG conversion, a section navigator built for one-thumb use, and the same aggressive autosave as desktop. On top of that, I built a native iOS app in Swift for field use, with things a web app can't do as well, like one-tap turn-by-turn directions to the property via Maps. The app is live on the App Store.

Both clients talk to the same backend and data model, but the iOS app is built for the conditions inspectors actually work in: no signal in a basement or on a rural property. It's offline-first. Answers and photos write to a local store and into a durable outbox, and a sync engine replays that outbox once connectivity returns, with anything the server rejects shown to the inspector instead of dropped. Creation calls are idempotent on a client-generated request id, so an outbox that replays after a timeout can't create an inspection twice. Template building stays web-only by design; the app is purpose-built for fast data capture in the field, not administration.

A section navigator with search and per-section completion counts replaces scrolling through a hundred fields, and photos can be annotated with circles, arrows, and labels before they land in the report.


Push notifications
The app now has a Notifications tab and sends push alerts when a client books, signs, or pays, with a separate switch for each kind so a payment doesn't interrupt a crawlspace. Push is sent over APNs directly from Cloudflare Workers, with no library involved: an ES256 JWT signed with jose and a plain fetch. The more important change was routing all six places that created notifications through one createNotification chokepoint, so "a notification exists" and "the inspector's phone was told" are the same event rather than something each call site has to remember.
Drafts: start in the driveway, finish at the desk
The app can start an inspection in seconds: just an address and a template. Everything else is left out on purpose — clients, fee, agent, agreement. That speed creates a gap, so these are real inspection rows flagged as drafts. Drafts can be filled out normally but can't be published, and creating one has zero side effects: no agreement email, no pay prompt, no booking link. A guided Complete Draft flow on the web collects the rest, pre-filled from the inspector's defaults. It's one server endpoint that mirrors normal inspection creation step for step, rather than a sequence of client-side PATCHes that could leave a row half-promoted. It's also the only place auditable can still be set, since locking on publish isn't a decision to make for someone on a GPS tap.
Full guide: Using Eave on iOS →Multi-User Organizations
Many inspectors work as part of a small business rather than solo, so Eave supports organizations: create one, invite teammates by email — even people who don't have an Eave account yet, via an invite link that activates on signup — and everyone in the org automatically shares templates, inspections, and clients. There's no manual sharing toggle per object; if you're in the org, you see the org's work.

That "automatic sharing" model was a deliberate simplification. A per-object ACL system would be more flexible, but for a small inspection business — usually a handful of inspectors covering for each other — it's also more complexity than the problem calls for. Org-level defaults (a default template, a default fee) let an admin standardize how the whole team works without policing every new inspection individually.

Stripe-Powered Billing
The free tier lets an inspector run up to 5 inspections with no card required and no time limit — enough to genuinely try the product on real jobs, with every feature turned on, before paying. Eave Inspect Pro ($10/month, price locked for the life of the account) removes the cap. Billing is per-seat through Stripe — one subscription per user rather than per organization — with Stripe's customer portal handling self-serve plan management.

Per-seat rather than per-org billing keeps the pricing model simple to reason about for both sides — an org isn't blocked from adding a teammate by a shared seat pool, and I don't have to reconcile who "owns" the subscription when org membership changes. I also moved deliberately away from a hard paywall: rather than middleware guarding every route and a subscription check scattered through every feature, the entire gate is one check at the single action that costs me anything — creating an inspection. Everything else, including data an unsubscribed user has already created, stays fully accessible. Far less code, far fewer ways to accidentally lock a paying customer out of their own reports.
Full guide: Subscriptions & billing →