On paper, Caira is a WhatsApp receptionist for hospitals and clinics. In practice she is the most patient colleague I have ever had: answers in English, Hindi, Telugu or Hinglish, at 3 am, to a hundred people at once, and she has never once sighed.
This is the story of how I built her, told the way I actually experienced it. Less “here is my architecture”, more “here is the thing that scared me, and here is the trick that made it boring”. The hard part was never getting a bot to answer. Any modern model answers anything, instantly and beautifully. The hard part was making it answer like an employee who knows the rules, admits what she does not know, and never, ever invents a consultation fee.
Before any theory, watch one real message go through the machine. A patient asks about migraine and timings. About 1.6 seconds later they have an answer the AI was never allowed to make up.
One request before we start: everything below is animated and every playground actually works, so press things. Break things. That is what they are for. If you are an engineer, the “For engineers” boxes carry the deep cuts. If you are not, skip every one of them and you will still get the whole story.
Meet Caira
Every hospital and clinic has the same problem. The phone rings all day, someone has to answer “what are your timings?” for the fortieth time, write names in a diary, and call people so they actually show up. That person is expensive, goes home at 7 pm, and can hold exactly one conversation at a time.
So I built that person as software, living inside WhatsApp. She answers questions, books appointments, sends reminders, offers freed slots to the waitlist, and checks on you after your visit.
And her first client is not a small shop. It is a big hospital: departments, many doctors, a front desk that never goes quiet. That scale is baked into every decision below.
The decision that shaped everything came right at the start: one copy of Caira serves every hospital and clinic. One apartment building with many locked flats, instead of a new house for every family. It looks like a cost decision on a slide. In reality it is the decision that caused almost every hard problem in this story, and honestly, most of the fun ones too.
▸ For engineers: the stack behind the numbers
TypeScript end to end, thin serverless HTTP routes in front, Postgres with row-level security underneath, a Redis cache, and Gemini as the current model behind a swappable port. Around them, rented services for messaging (WhatsApp Cloud API), Indic translation, payments, and per-call LLM cost and latency traces. Everything patient-facing flows through a handful of thin routes: an inbound webhook, a booking form endpoint, a payment webhook, and a cron drain.
How a message flows through the system
You type “do you treat migraine? and what are your timings” and hit send. Here is everything that happens before the reply lands, and I promise none of it is magic.
- WhatsApp delivers your message to us. Not to a phone on a desk. To a small program on a server.
- We check it is really from WhatsApp. The message carries a cryptographic seal and we verify it. Broken seal: thrown away, no exceptions, no hurt feelings.
- We work out which clinic you texted. We look at which of our numbers your message arrived on. We never believe anything inside the message about which clinic it is, because a stranger could lie.
- We ask the AI one narrow question: what does this person want? Not "answer them." Just "sort this into a box." The AI is a receptionist's ears here, not her mouth.
- If they want to book, the AI steps aside. A form opens inside WhatsApp: branch, department, doctor, time. Boring, predictable code reads the real appointment book. No AI touches your booking.
- If it is a question, we fetch the facts first. The clinic wrote down its real answers. We pick the handful most likely to be relevant and hand the AI only those.
- The AI must answer from those facts alone. If the answer is not in what we handed over, she says "let me check with the clinic" instead of guessing.
- We check her homework before sending. The reply is inspected, length-capped, and stripped of any link she tried to sneak in. Then translated into your language and sent.
- We remember the answer. The next patient asking about timings gets the same reply instantly and for free. The same few questions are most of the traffic, so most of the traffic is free.
The system map: who talks to what
When I sat down to draw the system, the first map had three kinds of humans on it: patients (WhatsApp only, they never see a website), clinic staff (a dashboard), and doctors (their own day view). Everything on the right side is a company I rent something from.
The rule I refused to break: every rented service sits behind a plug socket of my own design. If Google doubles Gemini’s price tomorrow, I change the plug, not the house.
▸ For engineers: why this map is load-bearing
A patient has no direct edge to the system: every inbound byte arrives via Meta, signed, which is what makes the tenant router trustworthy. And every outward edge is labelled with a port name (a model provider, a translation provider, a cache, a payments provider, a metrics sink, a stack of repositories), not a vendor name. The vendor is resolved in one place, the composition root.
How I structured the entry points
Zoom in one level and Caira’s building has only three doors that matter: one where WhatsApp messages arrive, one where the booking form talks to us, and one where a timer knocks to say “send today’s reminders”. Plus a small payment door round the back.
I kept the doors deliberately dumb. They check ID and hand you to the rooms inside. All the thinking happens in rooms that have no idea they are inside a building, which is why I can test every room without ever building a door.
▸ For engineers: what every route does before it delegates
An HTTP route can only be exercised by constructing a request, faking a signature, and standing up an environment. A pure function can be exercised by calling it. So every route in the edge layer does exactly four things, then hands off:
- Read the raw body before any JSON parsing: the HMAC runs over raw bytes.
- Verify identity: HMAC signature, RSA decryption, or a Bearer secret.
- Parse and validate the payload shape against a strict schema.
- Hand the validated object to a domain function, and translate its result into a status code.
How I keep clinics isolated from each other
Here is the bug that actually kept me careful: clinic A seeing clinic B’s patients. In healthcare that is not a bug report, it is a regulatory incident.
So how do I know which clinic a message is for? The tempting answer is “the message says so”. Wrong answer. Whoever sends a message chooses what it says. Instead I use the one thing a sender cannot choose: which of our phone numbers the message physically arrived on.
It is the difference between a visitor telling you which flat they live in, and you noticing which letterbox their post came out of. Only one of those can be faked.
▸ For engineers: the three backstops behind the letterbox
Resolving the receiving number to a clinic is one indexed lookup against a unique index, no table scan. From there every tenant row in the database carries its own clinic id, and every repository call takes that clinic id explicitly. Row-level security is the designed backstop: a small wrapper pins the current clinic onto the database transaction, and policies on the rows themselves refuse to serve another clinic's data, in case a developer ever forgets a WHERE clause.
Every AI agent is secretly just a loop
Now the AI concepts, and I will let you in on the industry’s best-kept boring secret. Strip away the hype and every agent does four things, forever: read what arrived (perceive), decide what is needed (think), do it (act), and confirm it worked (check). The loop, the rules and the checks around the model are called the harness.
The model is one part in the machine, and not even the biggest part. Caira’s harness is the receptionist’s training, checklist and manager. The model is just her vocabulary.
▸ For engineers: the loop, in real code
One function IS the loop: resolve tenant, detect intent, run the matched flow, and persist state only after a successful send. That last detail is a check most builders forget. Not a single vendor import inside it: every dependency arrives through a port, and the LLM is called from exactly one narrow place inside think.
How I stopped the model from making things up
Language models are fluent guessers. In most products a wrong guess is a quirk. Here, if Caira invents a consultation fee, a real patient brings 500 rupees to a 900 rupee visit, under a hospital’s name. So my central question was never “which model”. It was: how do I make her structurally incapable of answering outside the facts she was handed?
Step one, she only ever sees the right page, never the whole library:
A librarian does not read the whole library to answer you. She finds the one right page. Caira scores every clinic fact against your question and hands the AI only the winners.
Step two, I built three walls around whatever she writes:
Wall 1: the briefing
The AI is handed the clinic's real facts inside clear markers and told: answer using only these; if it is not here, say you will check with the clinic; never invent a price, service or time.
Cheap, and it works most of the time. But it is a request.
Wall 2: the inspection
Whatever comes back is treated as untrusted input from a stranger. It must be the exact shape we asked for, under a length limit, and any web link it tried to sneak in is deleted.
This is the wall that catches a model going off-script.
Wall 3: the fallback
If the AI is slow, broken, out of quota, or returns nonsense, we do not retry into a guess. We send "let me check with the clinic and get back to you."
Every failure becomes a human handoff, never a made-up number.
The subtle one
The clinic's map link is not written by the AI. The AI can only raise a flag that says "they want the location." Our code attaches the real link.
Which is why any link the AI produces is treated as an anomaly and deleted.
And because the internet is the internet, people will absolutely try to talk her out of the rules. Go ahead, you try:
Also try a normal question. The bouncer lets guests in. He only stops troublemakers.
How I designed memory: state vs recall
Memory sounds warm and fuzzy until you have to design it. Remember too little and “book me with the same doctor as last time” gets a blank stare. Remember too much and every message costs more and leaks privacy. There are two kinds of memory here, and most builders mix them up.
Your school keeps two things about you: a timetable (exact, which class you are in right now) and a report card (a short summary of who you are). Caira needs both. Timetable = state. Report card = recall.
📋 State (exact)
"We are on step 3 of booking." Stored in the database, never guessed, and saved only after a successful send.
🧠 Recall (summary)
"Last visit was Dr. Rao, prefers Hindi, evenings." A few summarized lines handed to the model so conversations feel human.
How I cut the LLM bill with caching
Two hundred patients ask “what are your timings?” and a naive bot pays the model two hundred times for one identical sentence. If ten friends ask you the same question, you do not re-think the answer ten times. You remember it. So does she.
▸ For engineers: the detail
How I handle vendor outages
Some day Gemini will have a bad minute. Every vendor does. In most products the AI feature just dies and takes the user experience with it. I decided early that a vendor outage is not allowed to become Caira’s outage: she downgrades to a simple keyword matcher and keeps answering. Simpler replies, zero cost, nobody left on read.
If the head chef is sick, the restaurant does not close. The sous-chef cooks a simpler menu.
The second half of surviving bad days is the letterbox. A phone call needs both people free at the same moment. A letter does not: drop it in the box, get your receipt instantly, the postman delivers when ready and retries if you are not home. All slow work goes through the letterbox, so WhatsApp never waits on a slow model.
▸ For engineers: the detail
How I test the bot: an exam before every deploy
Change one word in a prompt and a bot can silently get worse. Without a safety net you find out from an angry hospital. So Caira sits an exam before every deploy: fixed questions, known right answers, a score. The alarm below is the strict parent who notices the moment a 90 becomes a 60.
How I model conversations: a state machine, not vibes
My favorite piece of low-level design. Caira never “remembers the vibe” of a conversation. She knows exactly which square you are standing on: browsing, booking, confirming, cancelled. Every message either moves you along a printed arrow or does nothing. There is no square called “improvise”.
idle. The Flow variant collapses a five-state chain into one native form. The
teal box is the one you are standing in right now.
And every AI answer rides a conveyor belt: facts go in, the model writes, the shape gets checked, links get stripped, length gets capped. Anything weird falls off the belt into the safest sentence in the product: “let me check with the clinic and get back to you.”
The worst thing Caira can do is ask a human. She is never able to be confident and wrong. That guarantee is what most of the engineering is for.
▸ For engineers: the three walls, in code
The prompt fences the clinic's facts between explicit markers and asks for JSON only, a request the model can ignore. The response is checked against a strict schema, and the sanitizer strips every link and caps the reply length, an actual guarantee. The provider stack itself is a decorator chain, cache then fallback then metering then vendor, so a slow or dead model degrades instead of taking the feature down. Feed her a prompt injection and the classifier tags it, grounding still checks it against the clinic's real facts, finds nothing there, and the whole thing falls back to the same polite handoff a real failure would produce.
What actually costs money at scale
“Does it scale?” almost never means what people think it means. Computers are cheap and Caira is stateless, so more traffic just means more copies of her. The real question I kept asking myself: as this grows from one big hospital to a thousand clinics, which single thing bends first, and did I leave myself a lever?
Here is where the 1.6 seconds actually goes:
And here is the honest ladder, rung by rung, of what bends on the way up:
The punchline most people get backwards: at realistic volumes, sending WhatsApp reminders costs more than running the AI. The robot brain is cheaper than the postage.
The demo is easy. The product is everything else.
If you remember one thing from this whole page, make it this. A weekend and a model API gets anyone a chatbot that books appointments. The distance between that demo and something a hospital trusts with real patients is everything you just scrolled through: grounding so she cannot lie, isolation so clinics cannot leak into each other, metering so costs cannot surprise anyone, and graceful degradation so the product outlives the vendor.
The AI is the easy, swappable part. The building around her is the product.
Thanks for scrolling this far. If you pressed at least one button on the way down, we are officially friends.
Client details anonymized. The architecture is real but deliberately simplified: names, counts and several implementation details are changed or left out on purpose. Cost figures use published Gemini rates; token counts and traffic volumes are modeled estimates and are labelled estimate wherever they appear.