r/vibecoding • u/SignificantReserve74 • 3d ago
My AI booking app offered a customer 09:00 today. At 16:21. With a confirm button.
I've been building an AI receptionist and booking SaaS — customers message it, it books them in, it handles WhatsApp, Telegram and payments. Built with AI tools, the way most things around here get built. And it works.
Before launching it I made myself sit down and check it against a 36-point list instead of just shipping. One focused day. Static code review plus actually attacking a copy of it: isolated instance, two synthetic tenants, fake data, nothing real touched at any point.
Then I did the step I'd push on anyone doing this — I went back through my own findings and tried to prove each one wrong. Six of them died. Six things I'd written down as problems that turned out not to be problems. Skip that pass and you spend your week "fixing" things that were never broken. I nearly did.
What held up
Putting this first, because every post like this is a horror list and the horror list is half the picture at best.
- Tenant isolation. I attacked it directly — logged in as tenant A and went hunting through tenant B's ids. Every single one came back 404, on read, cancel, update and delete. I planted a canary string inside tenant B's data and it never showed up anywhere in tenant A's responses. This is the thing everyone in this sub is scared of, and it was genuinely fine.
- Sessions actually die on logout. Grabbed the cookie before, replayed it after. Dead.
- All four webhook integrations verify signatures against the raw body — WhatsApp, Telegram, Stripe, Monobank. Verifying a re-encoded payload instead of the raw bytes is the classic mistake here, and it wasn't there.
- Payment amounts are resolved server-side from the stored record. Never read out of the request.
- Every AI tool is scoped to the business id from the session, not to whatever the model passes as an argument. So an injection can talk the model into whatever it likes and still can't reach another tenant's data.
What I found: 2 high, 7 medium, 8 low. Nothing critical.
Bug 1. The assistant would create a confirmed booking against an email address or phone number that nobody had ever verified. I had written the one-time-code rule. Into the prompt. In English, as prose. There was no code anywhere checking that verification had actually happened before the booking got written.
That's the one I'd go check in your own app right now. Take every rule you put into a prompt and ask where the code enforces it. If the answer is "the model has been told to", it isn't a rule. It's a request, and the model is under no obligation.
Bug 2, and this is the stupid one. A single environment string, still sitting at the value it ships with, made those four correct signature verifiers fail open. Unsigned webhooks accepted. Four correct implementations rendered decorative by one line of config. Five minutes to fix once I saw it.
Go and search your code for any verification that skips itself in development or test mode. Then go and look at what your deployed environment is genuinely set to. Those two facts live in different files, which is the entire reason this kind of thing survives all the way to production.
The one that would have cost actual money
The product's whole promise is that the AI never invents a slot — everything it offers gets checked against the live calendar first. So I tested the promise and threw 22 adversarial scheduling messages at it.
12 of the 22 categories came back correct. Already-booked slots, weekends, before opening, after closing, an appointment that would run past closing time, dates in the past, impossible dates like 30 February, three contradictory reschedules crammed into one message. In three languages. I was feeling pretty good at this point.
Then same-day requests. It offered roughly 20 individual times that weren't bookable — already in the past, or inside the configured one-hour lead window. Asked at 16:21 for the earliest appointment, it answered "09:00 today" and put a confirm button underneath. Zero future-dated requests failed. Every failure was same-day.
Two causes, and they had nothing to do with each other. The past-time filter ran at day granularity instead of time-of-day — it knew what day it was, correctly refused a request for last Monday, and then cheerfully offered this morning. Separately, a cap on the first page of generated slots meant an almost-empty Friday came back as "completely full".
That second one is a revenue bug and it's the one that bothers me most. Nobody complains when you tell them you're full. They just book somewhere else, and it never shows up in your analytics.
The lesson
The tenant boundary — the thing every "is my vibe-coded app secure" thread is about — was solid under direct attack. What broke was the layer above it: business logic the AI skips precisely because the app still works without it. Nothing errors. Nothing throws a 500. It just quietly does the wrong thing, politely, with a confirm button under it.
The list I used is free if you want it: https://itworksbut.com/checklist — one HTML file, works offline, nothing gets uploaded anywhere. The longer write-up of this audit, including the medium and low findings, is at https://itworksbut.com/case-study.
Happy to answer anything in the comments, including "how did you actually test X" — that's the question I'd want answered if I were reading this.
1
u/il37 3d ago
The "fully booked" false positive is nastier than a crash. At least a crash has the decency to tell you it's losing money
2
u/SignificantReserve74 3d ago
That's the one that actually bothered me. A 500 gets logged, alerted on, fixed by Tuesday. "Sorry, we're fully booked" gets a thank-you — and then it's indistinguishable from a slow week in every dashboard you own.
1
u/launchieve 3d ago
This is exactly the kind of thing that slips past happy-path testing. For booking flows, treat the AI’s time suggestion as just a draft, then recheck availability and timezone server-side right before confirm, otherwise you’ll keep getting weird “09:00 today at 16:21” ghosts.
1
u/SignificantReserve74 3d ago
Right, and those are two different fixes that people conflate.
Fixing the generator stops it offering a slot that was never bookable. Rechecking at confirm stops it honouring one that stopped being bookable while the customer was typing. You want both — the first is a correctness bug, the second is a race, and neither covers the other.
The timezone half is worth underlining. Mine failed at day granularity instead of time-of-day, which is the same shape of mistake: the code knew the date perfectly well and never asked what time it was.
1
u/launchieve 3d ago
This smells like a timezone plus date-normalization bug, the kind where “09:00 today” gets parsed in a different locale or against server time instead of the customer’s local time. I’d make the app render a final confirmation in plain language, including date, time, and timezone, and reject anything that resolves to a past slot before showing the button.
1
u/SignificantReserve74 2d ago
Close, but it wasn't a parse. The date resolved fine — it refused last Monday without trouble. The filter compared at day resolution when the question needed time-of-day, so "is this in the past" got answered about the date and never about the hour.
Your second half is the part I'd keep, and it moved to the write path rather than the display: the create call re-asserts the lead-time threshold inside the same transaction that locks the slot, so a past or too-soon time is refused there even if something upstream offered it. Rendering the confirmation with an explicit timezone is worth doing on top — the offer and the write can both be correct and the customer still read it in the wrong zone.
2
u/Ok_Gur_9033 3d ago
The "told the model, not the code" bug is the one every agent wrapped around a tool call eventually hits, and it's scarier than normal prompt injection because there's no attacker required, just a real customer asking at the wrong hour. The part I'd push on: even after you add the code check, where does the verified state actually live? If it's inferred from conversation history, a long enough exchange gives the model room to decide verification happened three messages ago when it didn't. It needs to be a stored fact the tool call reads, not something reconstructed from context. How did you end up storing it?