Turned an AI-generated CRM into one that could survive real customers
An AI-built CRM demoed perfectly — until a few bugs led us to look underneath. Here's what a full production audit uncovered, and how we made it real.
- Client
- Muhammad A.
- Industry
- CRM / B2B SaaS
- Engagement
- Production Readiness Audit
- Duration
- 4 months

Key Results
- •Removed a single-vendor dependency that had been duplicated across 8 separate backend services, so leads and companies now exist natively in the product
- •Fixed a silent-failure pattern where one broken data reference had quietly reappeared in over a dozen places across the codebase with no error and no alert.
- •Reduced duplicate internal alerts from 2,271 to zero for the failure scenario.
- •Built a full production security baseline — role-based access, system health monitoring, audit logging, hardened credentials — where none existed before.
- •Consolidated logic that had been independently rebuilt roughly 8 times across services in three different languages into one shared implementation.
- •Roughly 95% incident reduction rate due to bugs with monitoring and complete internal automated audit.
- •Replaced single-vendor lock-in with a CRM-agnostic integration layer supporting HubSpot, Zoho, and Salesforce behind one interface.
The Client came to us with a demo that looked ready — and a handful of bugs they wanted fixed.
They'd built an AI-powered CRM for B2B sales teams — lead management, email outreach, and LinkedIn automation, with AI generating much of the outreach content itself — almost entirely using AI coding tools. It demoed well: a dozen backend services written in Go, Python, and TypeScript, a multi-tenant PostgreSQL database, and live integrations with HubSpot, Stripe, Twilio, and AWS. By most measures, it looked like a finished product.
During one of the demos, a few bugs surfaced — nothing that seemed unusual for a product at this stage. The Client asked us to take a look.
That's where the real story starts. What began as a short bug-fixing task turned into something much bigger the moment we started tracing why those bugs existed. They weren't isolated mistakes — they were symptoms. Underneath the surface, we found a system that had never been tested against the conditions real production traffic creates: real failures, real concurrent users, real data at scale. The deeper we looked, the more of the platform turned out to be built the same way — functional on the surface, unproven underneath.
We proposed stepping back to run a full audit before continuing to patch things one at a time. The Client agreed. What we found became the real scope of the engagement.
What "feature-complete" was hiding
The product couldn't stand on its own two feet
Software can work perfectly and still be quietly dependent on a single point of failure. In this case, the CRM used HubSpot's own ID numbers — HubSpot being the third-party CRM platform it integrated with — as its own core identifiers: companies, leads, and contacts were all keyed off IDs that belonged to HubSpot, not to the product itself. No company or lead could exist in the system without first passing through HubSpot. That same decision had been repeated across roughly eight different backend services, each with its own hand-built integration.
It's the kind of shortcut that's invisible in a demo and expensive months later. If a CRM vendor changes its pricing, has an outage, or a customer wants to switch providers, the entire product's foundation is exposed — and most teams don't find that out until it's already a problem.

Failures that never told anyone they'd happened
The more concerning pattern was software that reported success while quietly doing nothing. A database change made in one service was never propagated to the other services reading that same data — so ordinary, everyday actions, like opening a lead's details or pulling up enriched contact information, started silently failing. No errors. No alerts. Nothing visibly wrong. The team didn't find out until customers did, and said something.
That's the most dangerous kind of failure there is. It doesn't erode trust loudly — it erodes it invisibly, one unnoticed failure at a time, until someone finally asks why something that should have worked, didn't.
The parts nobody thinks to ask an AI for
Some of the biggest gaps weren't bugs at all — they were entire categories of production concern that simply hadn't been built. There was no way to monitor the system's health, so problems were only discovered after a customer ran into them. There was no access control: any logged-in user had full access to their company's data, with no way to restrict it. There was no audit trail — no way to answer "who changed this, and when" after the fact.
These aren't luxuries. They're the difference between software a team can trust with real customer data at real scale, and software that only works because nothing has gone wrong yet.
The same bug, living in four places at once
Those eight hand-built HubSpot integrations from earlier weren't just a dependency risk — they were eight independent copies of the same logic. Authentication, request handling, ID mapping, error handling: all of it written separately, once per service, in whichever of the three languages that service happened to be in. Nobody had decided to maintain eight versions of the same integration; it had just happened, one AI-generated service at a time, each one built with no memory of what the last one had done. Patch a bug in one, and the same bug was often still live in three others.
That's exactly why a handful of visible bugs couldn't be treated as a handful of visible bugs. Fixing what's in front of you doesn't fix what you can't see — and in a system built this way, most of it you couldn't see.
One schema per customer, and no ceiling in sight
Data isolation was a real requirement, not an afterthought — sales teams share sensitive pipeline data, and the Client didn't want one customer's leads visible to another's. When they asked their AI coding tools for the best way to isolate tenant data, the answer that came back was schema-per-tenant: every individual user, not even every company, was provisioned their own PostgreSQL schema. It's a textbook answer to the isolation question, and it worked — right up until scale started to matter.
There was no migration tooling behind it, either. Every product change meant running the same migration by hand, schema by schema, as raw SQL. At roughly 200 users, that was already a noticeable drag on every release. The model doesn't degrade gracefully past that point — it just keeps adding a schema, a migration target, and connection overhead for every signup, with no ceiling in sight.
The isolation requirement was never the problem. The architecture chosen to satisfy it was. We consolidated the schemas into a single shared database and moved data isolation into the database itself using PostgreSQL row-level security — the same guarantee the Client needed, enforced structurally instead of multiplied per customer. The migration folded roughly 200 existing user schemas into that one database without losing the isolation they started with.
These were the patterns that shaped the rest of the engagement — the most consequential of what a systematic audit turned up, not the only findings in it.

How we actually approached it
We didn't start fixing things immediately. Before writing a line of code, we ran a systematic, documented audit across every service in the platform, cataloguing every issue we found and ranking it by real-world severity — critical, high, medium, low — so nothing got missed and nothing got fixed out of order.
One moment from that audit stuck with us. A routine billing hiccup on a single customer's account had caused the system to retry the same failed operation thousands of times in under half an hour — and every one of those retries fired an internal alert. The team had been quietly flooded with noise about a problem only the customer could actually fix, and the customer themselves had never been told anything was wrong. Nobody had designed for what happens when a retry doesn't know when to stop. It simply hadn't come up — until it did, on a real account.

The same failure shape showed up somewhere we didn't expect: AI spend. The outreach engine called OpenAI to generate content, and retried failed calls through an in-memory queue — but OpenAI enforces rate limits the moment an account runs out of credits, and the queue had no way to tell "try again" apart from "stop, you're out of money." Every time the Client topped up credits, the queue immediately flooded OpenAI with everything it had been holding onto, burning through the fresh balance in minutes. We tore the in-memory queue out, moved the outreach pipeline onto the same AWS SQS backbone the rest of the platform used, and added a circuit breaker, exponential backoff, and a hard retry ceiling — the same fix, for the same underlying mistake, in a different part of the system.

Not everything we found needed the same treatment. Some services just needed the vendor dependency removed — the underlying logic was sound. A couple needed to be rebuilt outright, because the foundation itself was wrong, not just the implementation. Making that call correctly, service by service, is most of what this kind of work actually is.
What changed
The architecture no longer depends on a single vendor: the product now owns its own core identifiers, and all eight services reach CRMs through one integration layer instead of eight separate hand-built connections. That layer already supports HubSpot, Zoho, and Salesforce behind a common interface, so a customer can connect whichever CRM they actually use — and did, gradually, one vendor at a time — without anything downstream needing to change. The failure modes that used to fail silently now surface, get logged, and get flagged instead of disappearing. The production fundamentals that were missing — access control, monitoring, audit logging, credential hygiene, and hardened data handling — are now in place. And the logic that used to live in three or four slightly different copies now lives in one shared place, so a fix actually holds everywhere it's needed.

Where it stands now
The platform is live in production today, built to withstand exactly the kind of failure conditions that used to reach customers unannounced. The bugs that started this engagement are gone — and so are the hundred other things nobody was watching for until we went looking.
Here's the pattern worth naming directly: the bugs the Client originally asked us to look at were the tip of the iceberg. The audit found everything sitting underneath it. Most AI-built products are carrying that same hidden mass — they just haven't had a bug surface yet to force someone to go looking.
The Stack
Frontend
Next.js • React • TypeScript • Tailwind CSS • Clerk • Redux • SWR
Backend
GoLang (Gin, Echo) • Python (FastAPI) • Node.js/TypeScript (Playwright) • PostgreSQL • Redis • Celery
Integrations
HubSpot • Zoho • Salesforce • Apollo • Lusha • Stripe • Twilio • Clerk • Calendly • Groq / OpenAI
Architecture
Microservices • REST APIs • Event-Driven (AWS SQS) • Asynchronous Task Processing
Infrastructure
Docker • Linux • Reverse Proxy • AWS • Terraform
Testing & Reliability
Automated Testing • Data Validation • Circuit Breaker • Exponential Backoff • Alert Deduplication
Security & Observability
Role-Based Access Control • Audit Logging • System Health Monitoring (Sentry) • Credential Hardening (AWS Secrets Manager)
If you've built something with AI coding tools and it's never had a full audit, there's a good chance you don't know what's underneath it either.
We'll tell you — for free
Reach out at contact@graylining.com