A production conversational AI system for real-estate lead qualification over WhatsApp.
Sofía was built for SDM Capital to automate lead intake, property discovery, qualification, conversational context, and human handoff while operating against real business data and production workflows.
This public repository documents the engineering behind the system: architecture, LLM integration, reliability, concurrency, observability, production debugging, and technical trade-offs.
This is a sanitized engineering case study. Proprietary source code, credentials, customer data, private prompts, and production configuration are intentionally excluded.
Real-estate leads arrive through WhatsApp with incomplete, ambiguous, and constantly evolving requirements.
Sofía was designed to help SDM Capital:
- respond to incoming leads automatically;
- understand property preferences from natural-language conversations;
- search available properties using structured data;
- qualify leads using business rules and conversational signals;
- maintain context across multiple messages;
- process voice messages through speech-to-text;
- hand conversations over to a human when appropriate;
- operate reliably despite webhook retries, concurrent messages, malformed data, and external API failures.
- Runtime & Backend: Cloudflare Workers, JavaScript (ES Modules)
- Frontend: React, Vite, TypeScript
- Database: Supabase, PostgreSQL
- Messaging: WhatsApp Cloud API
- LLM: Anthropic Claude API
- Speech-to-Text: OpenAI Whisper
- Notifications: Resend
- Integration Layer: REST APIs, webhooks, PostgREST
WhatsApp Cloud API
↓
Cloudflare Worker
↓
Persistent message handling
↓
Lead-level processing and validation
↓
Claude tool use
↓
Supabase / PostgreSQL
↓
WhatsApp response
↓
Human handoff / notifications
The architecture is designed around a simple principle: the LLM can interpret and propose actions, but critical state, validation, business rules, and consistency are enforced by the application and database layers.
-
Replaced prompt-only JSON extraction with structured LLM tool use and runtime validation.
-
Added idempotency protections for repeated WhatsApp webhook deliveries.
-
Serialized message processing per lead to prevent race conditions and out-of-order state updates.
-
Established an append-only message table as the canonical conversation history.
-
Moved critical lead-qualification logic from probabilistic LLM output toward deterministic business rules.
-
Added production observability for errors, latency, token usage, cache behavior, and degraded results.
-
Introduced prompt caching based on measured conversation patterns to reduce LLM input costs.
One production issue originated from a field that represented bedroom count.
The database could contain values such as:
"1 or 2"
That value eventually reached a PostgREST filter that expected a numeric value.
The resulting flow was:
Ambiguous bedroom value
↓
Numeric PostgREST filter
↓
HTTP 400
↓
Error interpreted as an empty result set
↓
Sofía incorrectly concluded that no properties matched
The fix introduced three protections:
- integer-or-null validation before building the query;
- explicit logging for PostgREST errors instead of treating them as empty results;
- a controlled fallback when the filter cannot be safely applied.
The important lesson was not the malformed value itself, but the failure semantics: an infrastructure or validation error must never be silently converted into a valid business answer such as “no properties available.”
The system is already operating in production, but there are several areas I would improve next:
- introduce a transactional inbox/outbox pattern for stronger delivery guarantees;
- enforce fail-closed verification for incoming WhatsApp webhook signatures;
- further separate persistent lead profile data from temporary property-search state;
- improve conversational references to previously shown properties, such as “the second one” or “something similar”;
- complete shadow-mode evaluation before fully activating deterministic
readyandscorerules; - formalize data-retention policies for conversational and operational data.
The goal is not to add complexity for its own sake, but to strengthen reliability, explainability, and maintainability as usage grows.