Erik Pearson · Forward Deployed EngineerCase study

From complaint to deploy in a week

One August feature in Dental Prospector, the territory map I built and still run for three regional offices while working there in field sales, walked day by day. This is how the loop typically runs, and it is the reason I am looking at forward deployed engineering: the job is to sit inside the customer's world and ship into it.

2 minutes 50 seconds. Real commit messages from the repository; all product data shown is synthetic.

The loop

A rep calls. I write the spec that night. I ship it behind a version number, with a migration, a service worker bump and a what's new note in the app. Then I measure, and I call the same rep back. Typical time from the call to the deploy is about a week, and there is no ticket queue anywhere in it. Five hundred and eighty nine commits between February and September 2026 and forty nine minor versions, one developer working with Claude Code, all while carrying a full sales territory. That is a minor version about every four days on average, so a week from call to deploy is the usual pace, not the best case. No model runs in this fix, on purpose: the doctor names were already in the data and search could not see them, so the answer was a column and a trigger, not a prompt.

Prospector commits per week and minor releases, February to September 2026589 commits over 33 weeks and 49 minor releases, v1.2 on 2026-02-23 to v1.50 on 2026-09-22.255075100v1.2v1.10v1.20v1.40v1.50FebMarAprMayJunJulAugSepcommits per weekminor release, under the axis
Prospector, February to September 2026: 589 commits by week, and a green tick for each of the 49 minor versions on the day its release notes were committed. Dates and version numbers only; the repository is private.

One week, one feature

  1. Day 1

    The call

    A rep searched for an office by the doctor's name and got nothing. The person calling is a colleague, not a ticket. That night I wrote the spec on one page: what they said, what the data actually looks like, and what would make the complaint impossible.

  2. Day 2

    The data

    Search matched doctor names against a JSON field on each office. Months earlier, a speed update had stopped sending that field with the map load, to cut a payload of about 36 MB. Nothing errored. The doctor-name match just quietly stopped finding anyone whose name lived only in that field, so the fix had to happen in the data, not in the search box.

  3. Day 3

    The migration

    One flat doctor_names column, small enough to ride along in the fast load, kept in step with the JSON by a trigger and backfilled in one pass. The backfill only touches rows that still disagree, so running it twice is harmless, and the rollback statements sit at the top of the file.

  4. Day 4

    The client

    The app loads the new column and searches it. Earlier that week, a search with zero hits had started telling the rep who manages the office instead of showing an empty list, because an empty list is where reps stop trusting the tool.

  5. Day 5

    The deploy

    Version 1.46.2 ships on 19 August with the migration, a service worker bump and a what's new note inside the app. The same release fixed a panel that said "loading" forever when a fetch failed; it now says the load failed and how to retry. The version, the migration and the dates are in the commit log.

  6. Day 6

    The call back

    I call the same rep back and have them run the search that failed. Measuring is part of shipping; a feature nobody uses is a bug I own.

  7. Day 7

    The next complaint

    The next release, a day later, logs every search and location check that finds nothing, and I review that log. The next gap shows up before anyone has to call.

The migration

The fix lives in the data. This is migration 135, trimmed to the three statements that matter. The trigger function it names checks the JSON type in separate branches before it asks for an array length, because Postgres does not promise to evaluate an OR left to right.

alter table public.dental_offices
  add column if not exists doctor_names text;

create trigger sync_doctor_names_from_enrichment_trigger
  before insert or update of enrichment_data on public.dental_offices
  for each row execute function public.sync_doctor_names_from_enrichment();

update public.dental_offices d
set doctor_names = sub.computed
from ( /* one computed value per office */ ) as sub
where d.id = sub.id
  and sub.computed is distinct from d.doctor_names;

What it adds up to

I would like to do this next for your customers, with your platform, in their environment.