Connecting an AI bot to a production database: three layers of read-only
A support bot had to read live MySQL without ever writing a single row. Why I built three layers instead of trusting one safeguard, and how I masked sensitive data before it could ever reach the logs.
The problem
Most questions that reach a support team look alike: billing status, line details, fault records. To answer them the bot has to look at the live database; otherwise its answer is either stale or made up.
But a bot connected to a live database means that anything going wrong touches customer data directly. I had two requirements:
- The bot must never be able to write to the database.
- A customer's sensitive data must never reach the logs or the database in raw form.
Why one safeguard is not enough
The first idea is usually a single check: "Reject the query if it contains UPDATE or DELETE." On its own that is fragile. Queries can change shape through letter case, comments or nested expressions, and a small change in application code can silently switch the check off.
So I did not leave safety to one layer working correctly. Each layer assumes the previous one can be bypassed.
Three layers
- Table whitelist. The bot can only see tables that are explicitly allowed. A query aimed at any other table is rejected before it runs, so even in the worst case the reachable data stays limited.
- SQL Guard. Every query is checked for write statements before it executes. This layer stops mistakes early, with a clear error.
READ ONLYtransaction. The query runs inside a transaction that is closed to writes at the database level. Even if the first two layers are somehow bypassed, the database engine itself refuses the write.
The last layer matters most: it ties safety to the database's own rule rather than to the good behaviour of application code. The first two catch problems earlier, where they are easier to understand.
Masking sensitive data
Read-only access protects the database, but the conversation itself carries sensitive data too. During a chat a customer may type their national ID number, phone number, email or card number.
I catch these fields with regular expressions and mask them. For card numbers I also use Luhn validation: instead of treating every random 16-digit number as a card, it checks whether the number is actually a valid card number. That reduces both unnecessary masking and missed card numbers.
The key decision is where masking happens: before anything is written to the logs or the database. Sensitive data never hits the disk in raw form, so there is nothing to clean up later.
Rule engine and tests
I built the bot on a state machine defined in YAML rather than a model that generates free text: 16 flows, each with its own steps, validations and exits. That is part of the safety story too: the bot knows what to do from predefined flows and does not produce anything unexpected.
Because the flows live in YAML, changing one can silently break another. I wrote 107 tests to catch that; without them it would not have been possible to update the system safely.
When the bot cannot resolve something it does not drop the conversation: it opens a ticket in Zammad/Zendesk and hands off to a live agent together with the conversation history.
What I learned
- Split safety into layers and assume each one can be bypassed. Put the strongest layer in the infrastructure itself (here, the database engine), not in the application.
- Mask sensitive data at the source. Cleaning it out of logs afterwards is both hard and unreliable.
- Rule-based flows plus tests are safer than free generation in enterprise support. Being controllable is worth more than an answer that looks "smarter".
You can try the live bot: TaksimChatFun Live Support.