← All writing

Building a CRM That Answers Leads in Two Minutes

May 16, 2026·4 min read·By Hrutik

There's a statistic that drives the entire design of this product: contacting an inbound lead within five minutes increases conversion dramatically compared to contacting them within an hour. The team I built Starz CRM for was averaging over two hours — not because anyone was lazy, but because a lead arrived in one tab, the campaign lived in another, WhatsApp follow-up in a third, and the deal stage in a fourth.

The product thesis was simple: put capture, outreach, and campaign generation in one interface and the response time problem solves itself. My job was the frontend architecture and the revenue systems.

Optimistic UI, and the rollback you must write

Sales reps work fast. They advance six leads through stages in ten seconds. If each of those actions waits on a network round-trip, the interface feels like wading, and reps go back to the spreadsheet.

So every state-changing action applies locally first and reconciles with the server after. That's the easy half. The half people skip is the rollback: when the request fails, the UI has to walk back to the previous state and tell the user it did, or you've built something worse than a slow app — you've built a fast app that lies.

Rapid successive actions on the same record also create ordering races. Actions queue per record rather than firing in parallel, so a lead can't end up in a stage determined by whichever response happened to land last.

External webhooks are hostile input

Meta and WhatsApp webhooks deliver payloads with unexpected nulls, fields that change shape between API versions, and occasional data you have no business storing.

Everything inbound goes through a sanitization layer before it reaches application state: scrub what shouldn't be retained, validate against an explicit contract, and reject loudly rather than letting a malformed record poison the store. Treating third-party payloads as untrusted input — the same way you'd treat a form submission from a stranger — is the only posture that survives contact with a vendor's version bump.

Entitlements are a correctness problem, not a UI problem

The subscription paywall gates features by plan tier, with usage metering and thresholds. The temptation in an early product is to implement this as conditional rendering — hide the button on the lower tier and move on.

That's not a gate, it's a suggestion. Every gated capability is enforced as an entitlement check the client and server both understand, driven by one shared contract. Across every production release, zero payment gate bypasses. Hiding a button is a design decision; refusing the action is the actual feature.

Thirty test suites in a startup that ships weekly

I know how this sounds. Startup velocity is supposed to be the enemy of test discipline.

But I'd watched features break revenue-critical flows silently before, and the failure mode is brutal: signup or upgrade breaks, nobody notices for two days, and you can't even measure what you lost. So the rule was that revenue flows — signup, upgrade, campaign launch — get Playwright journeys in CI, and no feature flag flips on without them passing.

The suites run in under four seconds. Four major revenue releases shipped with zero regressions. The tests weren't a tax on velocity, they were what made shipping weekly survivable — you can move fast when you know what you broke.

Strict TypeScript contracts for analytics, entitlements, and lead schemas did similar work, with no any anywhere in the codebase. Most of the bugs that would have reached those tests never got written.

What went wrong

An early release leaked memory during long sessions. Reps work eight-hour shifts with the tab open, and thousands of lead change events accumulated in the client event store with nothing ever evicting them. By hour six the tab was sluggish; by hour eight it was unusable.

Fixed with a bounded circular buffer for event history and garbage collection of state nodes that nothing renders. Memory footprint stayed under 65MB across a full shift.

The general lesson: anything append-only on the client is a leak with a delay. If you're pushing into an array on every event, decide the eviction policy at the same time you write the push.

Results

  • Lead response time from hours to under 2 minutes via automated WhatsApp routing
  • 30+ test suites passing in under 4 seconds
  • Zero regressions across 4 major revenue releases
  • Zero payment gate bypasses in production

What I'd change today

TanStack Query from day one instead of hand-rolled synchronization hooks. I wrote caching, invalidation, and optimistic mutation logic that a library does better, and every hour I spent on it was an hour not spent on the product.

← Read more posts