It compiles.
It renders.
It's still wrong.
I'm a security researcher. I find the failures in AI-built apps that never throw an error.
Data anyone can overwrite · records that exist three times · gaps in what was never collected
create policy "anon_write" on venues for insert to anon with check (true); create policy "anon_update" on venues for update to anon using (true);
- What it looked like
- Row-level security enabled. Policies written. Dashboard green.
- What was actually true
- The anonymous key shipped inside the frontend bundle could rewrite every row in the table. Twenty cities of data, editable by anyone who opened developer tools.
- What it looked like
- Three records. Three distinct Google place IDs. Three independent sets of ratings and reviews, all genuine, all returned straight from the API.
- What was actually true
- One location. Google lists the trail, the peak and the trailhead as separate entries, so the import created three rows and every field in the database agreed they were different places. Anything that ingests Google Places inherits this: a restaurant with a second listing for its takeaway counter, a gym with one ID per entrance, a clinic listed once per doctor. No deduplication that compares IDs will ever catch it.
- What it looked like
- The revenue dashboard loaded in under a second and showed a plausible number. No error, no warning, no empty state, nothing in the logs.
- What was actually true
- The API caps a response at 1,000 rows unless you explicitly ask for more. Every total, every export and every monthly figure had been calculated from the oldest thousand orders since launch. Nothing in the app can tell the difference between "this is all of it" and "this is where it stopped."
Does any of this sound like your app?
- You're not sure whether one customer can see another customer's data, and you can't find out.
- It was fast with test data and it crawls now that it's real.
- You need to add one field and you're afraid to, because last time something unrelated broke.
- The same customer, product or place appears more than once and nobody knows which row is real.
- You're taking payments and you've never checked whether the price is decided on your server or in the customer's browser.
- Signups look weak, and nobody has confirmed that your verification emails are actually arriving.
- The app confidently returns something wrong, and there's no error anywhere to follow.
- Your AI assistant keeps proposing fixes. Each one is plausible. None of them ends the problem.
What I fix
security · three ways in
Security audit
A severity-ranked report of what's exposed: access control, data anyone can read or overwrite, secrets sitting in your client bundle, whether your checkout trusts a price the browser sent it, and what an attacker reaches once they have a login. The first pass is free and comes back within 48 hours. If there's nothing worth paying for, I'll tell you that.
Review & remediation
I audit, then fix what I found and verify it's actually closed. You end up with a secured app, not a to-do list. Work happens on a branch, never straight on production, and you get a written record of exactly what was changed and what was left alone.
Security retainer
Ongoing review as you ship. Every new table, endpoint and integration adds surface, and the fastest way to reopen a hole I just closed is to build the next feature on top of it. Monthly re-check, with findings before they become incidents.
while I'm in there
App rescue
Bolt, Lovable, Replit, Cursor, v0. The app got built. Then it met real users and real data. I go through the parts the builder never checks: who can actually read and write your tables, what your public key is allowed to do, which queries fall over at volume, and why a schema change breaks three things it shouldn't touch.
Coverage audits
The failure that isn't in your code and isn't in your schema. It's in what was never collected. Results that look complete because nothing can tell you what's missing from them. I work out what your data doesn't have, how badly it's distorting what users see, and what it costs to close the gap. In my own build this showed up as a dinner recommendation 43 km from the beach the app had just suggested — the code was right and the schema was right, but no coastal restaurants had ever been collected, so that was the closest thing that existed.
Data cleanup and record matching
Deduplicating a customer list. Merging two catalogues. Matching one system's records to another's when the names disagree and half of them are spelled three ways. I build the match, score every decision by confidence, and hand back the near-ties for a human to look at rather than guessing on your behalf.
AI workflow automation
Wiring a language model into a workflow takes an afternoon. Stopping it quietly producing nonsense at 3am is the actual job. I build the failure paths in: what happens when the model returns malformed output, when the API times out, when a field comes back empty and the next step doesn't notice. Lead capture into a CRM, document and email triage, scheduled reports, syncing two systems that disagree.
How it works
- You give me a look.Read-only access is fine. A repo link, a project, or just a description of what's going wrong — whatever you're comfortable sharing.
- You get a written report within 48 hours.What's broken, how serious each item is, what it costs to fix. This part is free. If you take the report and fix it yourself, that's a good outcome.
- You choose what gets fixed.Fixed price agreed before any work starts, so the number can't move on you. If the audit turns up nothing worth paying for, I'll tell you that rather than invent work.
The first report costs nothing and takes 48 hours.
Get an auditThree things I found the slow way
From two years building production apps almost entirely through AI coding agents — the same way most of my clients built theirs.
A public key that could rewrite twenty cities
Security looked configured, and technically it was. Both the insert and update policies checked true, which meant the key sitting in every visitor's browser had full write access to the entire dataset. Nothing behaved strangely, because nobody had tried yet.
A detector that would have flagged a quarter of anything
I specced a rule to catch overrated tourist traps. It fired on 27% of real venues, including five of the thirteen most famous places in the city. The maths explained why: as written, the same rule flags 28% of pure random noise. I killed it rather than ship a number that looked like a signal.
Half the records wouldn't match, and that was the answer
Matching 126 collected places against the live database recovered only 63. The instinct is to tune the matcher. The real answer was that the database had never held those kinds of records at all. The match rate wasn't a bug to fix, it was a map of what was missing.
Who you'd be working with
I built production apps the same way you built yours — prompting agents, shipping fast, trusting output that looked right. Then I spent two years finding everything that method gets wrong. Every fault on this page came out of that, and I found them because I was the one who had to live with them.
- PhDComputer science
- 5+ yrsCybersecurity research and building security products
- 10+ yrsWriting production code
- 2 yrsBuilding, breaking and repairing agent-built apps in production
No agency, no team, no account manager. You deal with the person doing the work. I'm in Taipei, which for most clients means you describe the problem at the end of your day and the report is waiting when you start the next one.