The data was there. The path to it wasn't.
A car dealership carries a lot of data: live inventory with pricing and availability, vehicle specs tied to VINs, trade-in valuations that change by the day, financing options, service slots. That data was spread across vAuto for inventory, TradePending for trade-in quotes, ChromeData for vehicle specifications, and each dealer's own CRM. None of it was surfaced to buyers in the moment they were deciding.
The buying conversation — what fits my budget, what would you give me on my trade, can you service it next week — was happening by phone or in the showroom, hours or days later. Dealers were losing leads who couldn't get an answer fast enough.
The brief: put a chat interface on every dealer's website that could answer those questions in real time, connected to the systems that already had the answers.
A visual constructor for dealers, a chat widget for buyers.
The platform had two distinct surfaces. Dealers used a visual flow constructor in the admin — a card-graph editor where they built conversation flows by connecting question cards, answer branches, and action cards: show inventory, fetch a trade-in quote, book a service appointment, capture a lead. No code required. Each dealer composed their own bot; the platform handled rendering and routing.
Buyers saw an embeddable chat widget on the dealer's website. The widget ran the configured flows, pulled live inventory from vAuto, surfaced TradePending trade-in valuations inline when a buyer described their current car, and captured contact details in ADF format for the dealer's CRM. The conversation shaped itself around what the dealer had configured and what the buyer was asking.
The platform was multi-tenant from day one. Each dealer got their own bot configuration, their own flow graph, their own inventory and CRM connection. One account could manage multiple dealerships. Auth, billing, and tenancy lived in MySQL; everything else lived in the stores that fit it.
- F · 01Visual flow constructor
- Dealers built conversation flows in a card-graph editor — questions, answer branches, inventory lookups, trade-in quotes, lead capture. No code required. Each dealer configured their own bot.
- F · 02vAuto inventory feed
- A Redis-backed queue job parsed vAuto FTP exports, updated MongoDB product data, and re-indexed Elasticsearch per dealer. Feed changes were isolated to one job.
- F · 03TradePending trade-in integration
- The chat widget fetched trade-in valuations from TradePending inline, surfacing a quote in the conversation when a buyer described their current vehicle.
- F · 04ADF lead export
- Buyer contact details captured in the chat were formatted as ADF XML and routed to the dealer's CRM. The industry-standard format meant no custom integration per dealership.
- F · 05Three-database backend
- MongoDB for evolving bot and inventory schemas, MySQL for multi-tenant auth and billing, Elasticsearch for per-dealer inventory search. Each store owned one job.
- F · 06Browser-automation test suite
- Laravel Dusk covered the full conversation flows end-to-end. Card-type and schema changes in MongoDB didn't break the widget paths; the test suite caught it if they did.
Three databases, each earning its place.
The decision to run three stores in parallel was deliberate and argued for on day one. The temptation on a project like this is to normalise everything into one store and accept the friction at the edges. We didn't.
MongoDB held the bot configurations and conversation flows — card types, branching logic, quick-answer options, the evolving inventory-display schema. That schema changed every sprint as the feature set grew. A relational migration on every card-type addition would have strangled delivery. MongoDB absorbed those changes without ceremony.
MySQL handled everything that needed transactional integrity: authentication, billing, and dealer tenancy boundaries. We weren't going to let eventual consistency anywhere near access control or subscription state. Elasticsearch indexed every active inventory product per dealer, enriched with vAuto feed data and the offers and interactives each dealer had configured. When a buyer asked about a specific car in the chat, the widget queried Elasticsearch — not a relational table. The recall quality matched what the use case needed.
Three databases isn't complexity — it's precision. One store trying to do all three jobs would have been the complex choice.
The vAuto integration ran as a dedicated queue worker. vAuto published inventory files by FTP; a Redis-backed Laravel queue job picked them up, parsed the feed, updated MongoDB with the new product data, and re-indexed the affected inventory in Elasticsearch. Each step was isolated. When vAuto changed a file format, one job changed, not the whole stack. The browser-automation test suite in Laravel Dusk kept the full conversation flows from regressing as card types and schemas evolved. 1,736 commits over roughly two years. The architecture held.
Two years in production; zero rewrites.
The platform shipped to the first dealers in late 2017. The multi-database design — which had looked like complexity on the architecture diagram — proved its value in operation. Failures were contained to their layer. A vAuto feed parse failure didn't affect billing. An Elasticsearch re-index didn't block a buyer conversation. Each store failed the way its type fails, and no more.
Dealer adoption held. When the widget asks questions instead of displaying a contact form, buyers answer. The trade-in flow — asking a buyer about their current car, fetching a TradePending valuation, displaying it inline — ran without requiring any dealer staff.
The project closed with 708 source files, a full PHPUnit and Dusk coverage suite, and a data layer the client's own team extended after handoff. New card types, new integration connectors — they added them without us. Three databases doing three jobs, and none of them doing someone else's.
