Case study · Product & engineering
Alcorte Alemán
Running a 50-year-old butcher shop through WhatsApp
Solo architect and engineer · Product definition, system design, implementation, operator onboarding
Phase 0 closed, Phase A1 in progress
An AI order-management system designed around one constraint: the operator will never open a dashboard.
At a glance
- Client
- Alcorte Alemán — family butcher shop, Mexico City, 50+ years in operation
- Role
- Solo architect and engineer — product definition, system design, implementation, operator onboarding
- Scale
- 120+ products in catalogue, 1000+ customers
- Problem
- Digitize order intake and daily operations without asking a lifelong operator to change how he works
- Approach
- WhatsApp as the entire interface — for customers and for the business owner
- Stack
- WhatsApp Business API → Meta webhooks → Vercel (Next.js) → Anthropic Claude → Supabase (Postgres + RLS)
- Status
- Phase 0 closed, Phase A1 in progress. End-to-end pipeline live in production; operator toolset validated against real data
- Repository
- Private, 2FA-enforced. Production on Vercel
The problem
Most small-business software fails the same way: it assumes the owner will learn a new interface. The shop's owner-operator has run this butcher shop for over fifty years. He knows his products, his margins, his regulars, and his suppliers better than any system could. What he does not have is an hour a day to spend learning an admin panel — and roughly one hour a day is the honest ceiling for how much time he can spend interacting with any system at all.
The shop takes a modest volume of daily orders, mostly from home cooks with occasional restaurant clients. Orders arrive by phone and by WhatsApp message. Nothing is logged. Prices live in the owner's head and on handwritten sheets. Delivery timing is estimated by feel. It works — until it doesn't scale, or until the one person holding the knowledge is unavailable.
The obvious solution is a web dashboard with an order queue and a product catalogue. The obvious solution would have been abandoned in a week.
The reframe
I inverted the requirement. Instead of “build an admin panel and teach the owner to use it,” the brief became:
If the owner-operator ever has to open a browser to run his business, the project has failed.
WhatsApp is not a channel bolted onto the product. It is the product surface — for customers placing orders and for the owner configuring the system. Changing a price, marking a cut as sold out, and asking for the day's summary all happen in the same chat app he already keeps open all day.
This single constraint drove every architectural decision that follows.
Design principles
These were fixed before a line of code was written, and every subsequent decision was tested against them.
WhatsApp-first configuration
Every system setting must be modifiable by the operator through a chat message. This is backed by a vendor_settings table rather than environment variables or hardcoded config — if a value can change, it must be changeable from a phone.
Operational language, never strategic jargon
The system speaks to the owner-operator the way a good employee would: concrete, day-to-day framing. Never “conversion rate,” “ROI,” or “NPS.” “Seventeen orders today, three still to deliver” is a sentence he can act on. “Fulfilment rate 82%” is not.
Never invent business facts
The bot must not state a price, a delivery zone, or a piece of shop history it cannot source from the database. This is enforced as a hard system-prompt constraint, not a soft guideline — a plausible-sounding wrong price is worse than no answer, because it becomes a promise the shop has to honour.
Manual-first automation
Nothing runs unsupervised until the data proves it should. Auto-mode unlocks only after high-confidence signal accumulates; every automatable step ships first as a button someone presses.
Synchronize preparation with arrival
Meat prepared too early degrades; prepared too late holds up the driver. Timing calculations are realistic rather than optimistic, and paired with proactive communication when they slip.
Passive learning from escalations
Every time the system hands a conversation to a human, that exchange becomes retrievable knowledge (RAG) rather than a discarded failure. The system improves as a by-product of being used, without a labelling workflow anyone has to maintain.
Build the accounting architecture early, implement it late
Mexican tax invoicing (CFDI) is a solved-in-theory, painful-in-practice problem, and the decision on self-issuance vs. an external accountant isn't mine to make. So the schema accommodates it; the feature waits.
Architecture
vendor_id through a get_vendor_id() helper, so the schema is multi-tenant from day one. Rebuilt for this case study.Why this shape
The webhook handler is stateless and deployed on Vercel, so there is no server for a one-person team to keep alive. State lives in Supabase with row-level security keyed on vendor_id — the architecture is multi-tenant from day one even while serving a single shop, because retrofitting tenancy is a rewrite and anticipating it is a WHERE clause.
The agentic layer
Rather than a rigid intent classifier, message handling runs a full agentic loop: the model receives the message plus a UserContext, and decides which tools to call. This matters because real messages from a butcher are not well-formed commands. “El sirloin súbelo a 320” and “ya no hay arrachera” are both configuration changes, expressed as speech.
Identity and permissions
The system distinguishes customers from operators by looking up the inbound phone number against an operators table, producing a UserContext that flows through the request. Six tools are exposed, split by permission tier:
get_product
search_products
list_products_summary
update_product_price
set_product_availability
get_today_summary
Permission gating is defense-in-depth: the tool set offered to the model is filtered by context and each privileged tool re-validates the caller before executing. Relying on the prompt alone to keep a customer from changing prices is not a security model — it's a hope. A message from an unknown number cannot reach a write path even if the model is talked into trying.
Every price change writes to price_history with changed_via: "whatsapp_tool", so a chat-driven edit is as auditable as one made through any other interface.
The escalation chain
Operational reality is that the bot will not handle everything, and the failure mode of an AI assistant with no exit is a frustrated customer.
- Owner-operator
- →
- Shift supervisor
- →
- Additional staff
- →
- dev_backup · non-removable
Escalation runs owner-operator → shift supervisor → additional staff → me as a non-removable dev_backup. The chain is configurable by the operator, with one exception: the final link cannot be removed.
A system that can be configured into a state where nobody is listening is a system that will eventually be configured that way.
What shipped
Phase 0 — foundations (closed)
vendor_settingstable with ten defaults, delivered as migration001_vendor_settings.sql, following the existing RLS pattern rather than introducing a second one.- Meta WhatsApp Business API integration with a permanent System User token scoped to
whatsapp_business_messagingandwhatsapp_business_management. - End-to-end pipeline confirmed in production: WhatsApp → Meta → Vercel → Claude → WhatsApp.
Phase A1 — operator toolset (in progress)
- Six agentic tools with the full loop,
UserContextidentity resolution, and permission gating described above. - Three validation tests, all passing against real infrastructure rather than mocks:
- Operator identification by phone lookup
- Live product price query against Supabase
- Price modification with a confirmed audit trail in
price_history
Operator onboarding artifact
Getting fifty years of undocumented business knowledge into a database is an interview problem, not an engineering one. I produced five structured PDF interview documents for the owner-operator using a three-colour coding system:
- Green — draft answers I had pre-filled, for him to confirm or correct
- Amber — business-specific data only he knows, blank by design
- Grey — internal pricing data deliberately excluded from the bot's knowledge base
Pre-filling the green fields turned a blank-page task into a review task. Marking the grey fields explicitly meant the boundary between “the bot may say this” and “this is internal” was a decision made once, on paper, rather than re-litigated in every prompt revision.
Engineering problems worth describing
A single invisible character took down authentication
The API integration failed with authentication errors while the credential was, by inspection, correct. The cause was a non-printing Unicode character (U+2028 class) introduced by copy-paste into the environment variable. It survived every visual check because it is, definitionally, invisible. Fix: sanitize credentials before passing them to any SDK constructor — .replace(/[^\x21-\x7E]/g, '') — and treat environment-variable hygiene as a first-class concern rather than a configuration afterthought. The general lesson: when a credential is “obviously correct” and still fails, stop reading it and start inspecting its bytes.
Temporary tokens are a trap you set for yourself
The second blocking bug was an expired WhatsApp API token — the default artifact of following a getting-started guide. A permanent System User token with explicitly assigned assets and permissions has to be configured deliberately, and the cost of not doing it is a system that works in development and dies silently in production, weeks later, at the worst moment.
Postgres array filtering has exactly one correct shape here
Product aliases are stored as text[] (a cut of meat has many names, and customers use all of them). Filtering requires aliases.cs.{value} inside a single .or() call — chaining two .or() calls produces a query that looks right and returns wrong results. Related schema decisions that shaped the tools: base_unit is a unit_type enum, soft deletes use deleted_at, and availability changes must write both availability_updated_at and availability_updated_by to preserve audit integrity.
Guardrails belong in the system prompt, not the knowledge base
Two constraints were deliberately promoted from “content” to “hard rule”: no medical or dietary-health advice, ever; and credit/fiado decisions are a structured business rule tied to customers.credit_limit_cents, not something the bot recites from free text. A butcher shop extending informal credit is a real, load-bearing business relationship. Getting it wrong costs money and trust, so it is enforced in code and schema rather than in language the model is asked to remember.
How the work was organised
The project runs across three deliberately separated channels, because mixing them degrades all three:
- Architecture channel — strategy, phase definition, and open design decisions. Slow, written, decisions recorded.
- Debug channel — bug diagnosis, screenshot interpretation, code-level fixes. Fast, disposable.
- Local execution — Claude Code against the repository.
Planning sessions precede construction and phases are formally defined before implementation begins. Work moves as reviewable, approval-gated steps with screenshot verification checkpoints, not as unreviewed batches of code.
Roadmap
- A2.1
- Customer-facing bot
- A2.2
- Intelligence and observability — funnel analysis, conversation logging
- A3
- Costs, margins, statistics
- Operación
- Live daily operation with the configurable escalation chain
- Decisión
- Strategic checkpoint after 3–6 months of real data, presented in operational framing
- Logística
- External delivery integration (Uber Direct, iVoy, Rappi Cargo) behind a Provider abstraction, manual-first via dashboard buttons
- B
- Voice AI — Vapi.ai as primary, ElevenLabs under evaluation. The owner-operator has agreed to record his voice for cloning
- C
- Full e-commerce
- D
- Accounting — pending the accountant's decision on CFDI self-issuance
The Decisión phase is the one I would point to as the design tell. There is a checkpoint, months out, where the accumulated data gets reviewed and the roadmap can change direction — and the materials for that review are specified now as operationally framed, because a strategy deck full of funnel charts would be a document the person it's for cannot use.
What this project demonstrates
Constraint-driven architecture
“No dashboard” is not a limitation to work around; it is the specification. Taking it seriously produced a cleaner system than the version that would have hedged with a web admin panel nobody opens.
Production integration discipline
WhatsApp Business API, Meta's token model, Vercel, Supabase RLS, and a tool-calling model, wired end to end and validated against live data — including the unglamorous failures (invisible characters, expiring tokens) that separate a demo from something that runs on a Tuesday morning.
Security as structure, not instruction
Permissions are enforced by context filtering and per-tool validation. Audit trails are written by the write path. The model is never the last line of defense.
Designing for a non-technical operator, seriously
Colour-coded interview PDFs, operational vocabulary, a non-removable escalation backstop, and an explicit decision to defer accounting until the accountant weighs in. The hard part of this project was never the tool-calling loop.
Building for a business that already works
A fifty-year-old shop does not need to be disrupted. It needs the parts that live in one person's head to become durable, without changing how that person works. That is a narrower and more interesting problem than greenfield.