Construction & property development · 2026 — ongoing
Innova Developments
Four disconnected apps became one, then the phone learned to take a service request.
Innova had four separate web apps, three copies of the same database client, and a service desk that ran on whoever happened to answer. The work started as a merge and turned into the thing this studio is actually about: getting every way a job arrives into one shape, then letting AI handle the parts that were costing a person an afternoon.
- Engagement
- Ongoing — merge, then platform
- Scope
- Consolidation, service intake, AI voice agent, vendor tendering, mail ingest
- Team
- Solo, working directly with the owner
- web apps, one deploy
- 4 → 1web apps, one deploy
- vendor accounts brought into one directory
- 1,309vendor accounts brought into one directory
- intake channels normalised to one record
- 5intake channels normalised to one record
Start by counting, not by rewriting
The brief was "merge these four apps." Before touching anything I read the repo and wrote down what was actually true: every app was already on the same Next.js and React versions, all three database-backed apps already pointed at the same Postgres instance split cleanly by schema, and exactly one route collided across all four — the root path. That inventory is what turned a month-shaped project into a weekend-shaped one.
It also surfaced the thing nobody had asked about. The Twilio number the plan called for retiring was no longer primarily Innova's — of the last 54 messages it sent, 30 were shift notifications for an unrelated business, going to thirteen real phones as recently as the day before. Releasing that number would have quietly broken a live app used by real people. Innova moved onto its own account instead, and the old one stayed exactly as it was.
Checking the account before touching it changed the answer. That is most of the job.
Every channel becomes the same record
A service request reached Innova five different ways — a phone call, a text, an email, a request filed in a third-party portal, and a person walking up to a broken appliance. Each one was handled differently, which meant none of them could be measured, triaged or chased consistently.
So the web form became the single entry point, and everything else became a translator into it:
- A QR sticker on the side of the appliance and inside the maintenance manual — the shortest path on the whole chart. The client's phone lands on the form with the problem in front of them.
- An inbound SMS auto-reply that doesn't just acknowledge, it hands back the link to the faster path.
- An hourly job that pulls new requests out of the third-party portal and files them in Innova's own shape, de-duplicated so re-reading an open request doesn't create a second one.
- An AI agent on the phone line (below).
Downstream of that single record sits a triage the owner already ran in his head — warranty or not, emergency or not, accept or decline — now written down, so the same request gets the same answer whoever is looking at it.
The phone agent
A caller dials the service line and an AI agent collects five things. When the call ends, a row lands in the same table the web form writes to — not a transcript in a folder, not an email to sort later, the same record, indistinguishable from one typed by hand.
The design decision that makes it reliable: the model does not decide what the agent says. Twilio handles speech-to-text and text-to-speech; the model is called once per turn to interpret one answer into one clean field. The script is a script. That keeps the failure mode small — a misread field, not a conversation that wanders — and it keeps the call cheap.
The oracle pattern
Innova's contacts live in JobTread, which is the system of record right up until someone here corrects a wrong phone number. The usual answers are both bad: let the sync overwrite the correction, or freeze the contact and watch the rest of it go stale.
Instead, editing a cell locks that one field. Locked fields are never overwritten; every field nobody has touched keeps tracking JobTread and refreshes on open. One contact can hold a phone number that's ours and an email that's theirs at the same time. When a locked field disagrees with the upstream value, the sync records a conflict and changes nothing — it never picks a winner on your behalf.
That same shape — pull it in, attach it to the entities we already have, never silently overwrite a human — is now being pointed at the mailbox. Ingesting mail through the Graph API rather than forwarding rules backfills five years of history instead of starting the clock today, keeps threading exact, and is scoped by an access policy to a named set of mailboxes, which is the honest answer to "so this app reads everyone's email?" It doesn't. It reads the mailboxes you put in the group.
Tendering, built and deliberately inert
The tender tool takes a project, a scope and a set of files, and emails a package to a filtered slice of the vendor directory. Syncing that directory produced the numbers that shaped the whole design: 1,309 vendor accounts, 404 with an email address, 905 unreachable. A tool built for 1,309 vendors and a tool built for 404 are different tools.
It shipped with sending switched off. Every invitation is recorded, every quote link is minted, the exact outgoing email is rendered for review — and nothing leaves until an environment flag says so. The blast radius is several hundred real subcontractors and a send cannot be recalled. Anything with that shape gets built visible and inert, then switched on deliberately.