Decision paper · Tuesday 18 August 2026
Ken asked for the pros and cons of building our own CRM, and for nothing to be built. This is that paper. The short answer is: don't start — but not for the reason you'd expect. Six pieces of this business have already left GoHighLevel since 5 August, one at a time, none of it ever called a migration. The risk isn't staying too long. It's stopping something that is working in order to do it all at once.
“The thing I don't want to do is get rid of GHL's email. I do want to replace their CRM, their other functions, but the email has to stay.”
That is a liability position, not a preference. A third-party processor absorbs the spam, unsubscribe and bounce exposure. Take it in-house and we own it.
Traced through the code on 12 August: every consumer-facing message goes through GoHighLevel — booking confirmations, reminders, review requests — and GHL owns the unsubscribe link, the unsubscribe status, and bounce suppression. Our own Telnyx sender only ever texts the inspector or our own staff, never a homeowner.
So the real constraint is: GHL keeps everything that touches a consumer's inbox or handset. Anyone scoping this later must read it that way, not as “keep the templates”.
We have been leaving GoHighLevel for months, one piece at a time, without ever calling it a migration.
| What | Where it lives now | Since |
|---|---|---|
| Availability / the diary | Our own code, from the onboarding record | 5 Aug |
| Double-booking gate | api/ghl/slot-claim.js — ours | 12 Aug |
| Client setup (Brain 2) | onboarding.json, data branch | ongoing |
| Call transcripts | Telnyx → our endpoint | 27 Jul |
| Call recordings | Telnyx, played in the portal | 13 Aug |
| Booking edits | The Inspector Portal | 5 Aug |
api/ghl/availability.js opens with Ken's own reason: “We need
our own calendar managing our operating system inside — I don't want to peek at
the ISN.”
This changes the question. It is not “should we build a CRM”. It is “should we stop finishing something we are two-thirds through, in order to do the rest all at once” — a smaller and much better-understood question.
| Function | Module | Replacing it |
|---|---|---|
| Contacts — the client record | contact-record.js | Medium. It is a table. |
| Work order · transcript · report | contact-record.js | Easy — notes on a contact |
| Appointments → inspector's phone | calendar.js | ⚠️ Hard. See below. |
| Consumer email & SMS | GHL | 🔴 Not replacing — Ken's ruling |
| Sub-account per client | agency.js | Only needed while GHL is the CRM |
| Workflows / automations | GHL UI | ⚠️ No API. Ever. |
| Payments / invoices | GHL + Stripe | Never run end to end — no client has connected a Stripe |
api/ghl/calendar.js turns a booking into a real appointment
on the inspector's GHL calendar, which two-way-syncs to their Google
Calendar and therefore their phone.
That is not a database row. It is the thing that makes an inspector trust us — Chad looks at his phone and the job is there. Replacing it means a two-way calendar integration per inspector, or asking inspectors to check a web page instead. “Check our portal instead of your phone” is how you lose a client who was otherwise happy. Nothing else on this list is close to it in risk.
GoHighLevel has no workflow API — confirmed in the 13 July MCP
audit and again on 12 August. Every automation for every client is
hand-built in the GHL screen, by a person, forever. No agent can ever do
it. GHL_CLIENT_SNAPSHOT_ID is unset and all six accelerant
snapshot ids are empty, so today each new client's CRM is built bare, by
hand.
This is the strongest argument here, because it does not improve with scale — it gets linearly worse. At 13 testers it is 13 hand-builds.
The onboarding record is the source of truth for prices, services, schedule and inspectors. GHL holds a copy of the outcome, not the definition.
GHL accepts an unknown custom field id silently and drops the value — that is what made Ted's first six bookings arrive hollow on 29 July. We refuse rather than post now, but the trap is theirs.
An inspection report is filed as a link, not an upload, because GHL's Files API needs media scopes we do not hold. Audio can never be a file in that CRM. Ken's USP — work order, transcript and report on one record — is being delivered around a limitation rather than through a feature.
Nothing on the launch list is blocked by GHL. Chad and Ted are blocked on an onboarding form, a phone that sounds right, and four Hamming assertions. A migration would consume the exact weeks those need.
Unsubscribe handling, bounce suppression, STOP processing, and the TCPA exposure on customer-uploaded lists. A half-migration that accidentally drags a consumer message out of GHL is a legal problem, not a bug.
Both already have working sub-accounts. And the confidentiality worry that once justified urgency turned out not to be true — Quality and GC have had separate sub-accounts all along.
Any savings figure produced today would be invented, and this business has been bitten twice by numbers that were read rather than measured.
From the record:
| Item | Figure | Source |
|---|---|---|
| Josh, per minute | $0.064 | scripts/cost-per-minute.mjs, 16 Aug |
| Web agent, per session | $0.14–$0.19 | 11 Aug costing |
| Telnyx recording | $0.002/min, storage $0 | 13 Aug |
What we pay GoHighLevel. Not in the repo. It is on an invoice.
What a migration would cost in hours. The build board reads
zero accounts — it filters status === "submitted" while
Chad is draft and Ted is active. Neither came
through the pipeline, so nothing about their setup was ever timed and it
cannot be reconstructed.
That second gap is cheap to close: a minutes box on a support
ticket, a started/finished stamp per build-board step, and a “who did it”
picker. About an hour — and then it measures itself. Without it, this
decision gets made on feel at the 13th tester too.
Do not build. Ken is right, and the reasoning holds up better than “bigger fish to fry” suggests. Three things are worth doing that are not a migration:
Worth doing now
At the 13th onboarded tester, as Ken said — with the timing data in hand. Three things would move the answer sooner:
| Trigger | What it changes |
|---|---|
| GHL ships a workflow API | The strongest argument for leaving evaporates |
| An inspector asks for something the CRM can't do | The Files API limit is the likely candidate |
| The V1 token cutoff lands unprepared | Not a decision any more — an outage |
The reason this paper recommends waiting is not that in-house is wrong. It is that we are already doing it, incrementally, and it is working — the diary, the slot gate, the transcripts, the recordings, the portal, the client setup. Each one left GoHighLevel when there was a concrete reason, and each one shipped in days rather than months.
The failure mode to avoid is not “staying on GHL too long”. It is stopping everything to do a migration we have been completing successfully by not calling it one.