60 FPS
Nathan Atherton
Back to Blog

I Got Into a Vibe-Coded App. The Dev Actually Fixed It.

Nathan Atherton· Staff Software Engineer26 August 20268 min read
On this page

My dad rang me a couple of weeks ago and asked if I'd have a look at an app his mate had built. Small local sports league app, Android only, the mate had spent a couple of weekends on it, sent it round the league by email as an APK. My dad wasn't worried about it, he just knew I'd know if it was fine.

I've had this conversation before. It usually ends with me spending twenty minutes poking at something, telling the person it's mostly fine, and moving on. This one didn't go like that.

The setup#

Before I get into what was actually in it, a quick word on the shape of the thing, because it matters. It's a React Native app. Backend is Supabase, which for anyone who hasn't seen it is basically "hosted Postgres with a REST layer bolted on and an auth system, but the good bits are the defaults." Very common stack for a solo dev shipping a small app in 2026. Realistically the whole thing was built with an AI assistant. The dev is a hobbyist, not a developer, and hadn't asked anyone to look at it before me.

The thing about Supabase is that its access model relies on Row Level Security being turned on for every table you don't want strangers reading or writing. It's not on by default when you create a table through the dashboard. You have to remember to switch it on and write the policy. If you don't, the anon key that gets shipped inside your app can read and write anything the client-side SDK has been told about. Which turns out to be, well, everything.

Anyway. I set aside an evening.

What I did#

I don't want to write a tutorial. There are plenty of those. But the methodology matters later so it's worth naming the steps:

  1. Pulled the APK apart with apktool to see what was inside.
  2. Ran it through apk-mitm to patch the certificate-pinning so a proxy could read the HTTPS traffic.
  3. Booted an Android emulator, installed the patched build, pointed it at mitmproxy on my Mac.
  4. Opened the app, tapped around, watched the requests.
  5. When the emulator + proxy setup got clever (it always does), I fell back to strings on the compiled bundle, grepped for sb_publishable_, found the anon key in about ten seconds.
  6. Fired the key at the Supabase REST endpoint with curl from my terminal.

None of that is exotic. It's a hundred blog posts stitched together, all of the tooling is free, and someone competent with an afternoon and a search bar could do the same. That's the point, really. The bar is low.

Total time: about an evening for the first audit, a second short evening to write the report.

What I found#

The anon key opened the door on every table. All of them. I want to be careful with numbers here because the app is small and identifying, but the shape was:

  • The players table had a few hundred rows and every one included the player's phone number, email address, admin role, push-notification token, and a boolean recording whether they had accepted the GDPR notice. That last one was quite funny in a bleak way. The app was recording consent while leaking the data anyway.
  • The fixtures table was fully readable. Not just readable, though. I confirmed I could UPDATE fixture rows too. So an attacker could rewrite match scores, retrospectively.
  • Same for teams, competitions, seasons, frames. Full read, full write.
  • I tested INSERT and DELETE against the players table by creating a fake player row and then deleting it. Both worked. So anyone with the APK could either add fictional players to real teams, or wipe every player in one call.
  • The only bit of RLS anywhere was on INSERT to the news table. Everything else was wide open.

To be clear, I wasn't storing any of the data. I ran the read requests, saw the shape, ran the write tests with strings like __ignore_test on the fewest rows I needed to confirm the class of vulnerability, and rolled everything back before I wrote the report.

But the shape was unambiguous. Anyone who had the APK, which the dev was distributing by email to the whole league, could dump every player's phone and email in one curl, and could delete or rewrite anything in the database on a whim.

That's a GDPR breach and it's a data-integrity nightmare on top. Not because the anon key was in the APK. That's fine, the anon key is meant to be public. It's a breach because RLS wasn't on.

Writing the report#

I wrote it up as a proper security report. Executive summary, findings in a table, reproduction commands, fix guidance, the lot. My dad passed it to his mate.

The critical bit of the report wasn't the list of what was broken. It was the fix pattern. Something like:

  • Enable RLS on every public table with alter table X enable row level security.
  • For anything with PII (like players), don't just RLS the whole table and hope. Create a players_public view that selects only the fields safe to expose (name, team, role, dates), grant SELECT to anon on the view, and point the app at the view instead of the table.
  • Deny writes to anon on every public table. Auth-gated writes go through a policy that checks auth.uid() against an admin flag on the user's row.
  • Rotate the current anon key, because as of me finding this it's been on my hard drive.

If I hadn't given the pattern, the dev would have gone to his AI assistant, asked "how do I fix this", got a plausible-looking answer, and possibly ended up with either a broken app (RLS on with no policy blocks the app's own reads) or an over-permissive fix that didn't actually solve the problem. This is important. AI is very good at giving you code that runs. It's less good at giving you code that resolves an ambiguous security requirement correctly the first time. If you're going to hand a vibe-coded app a fix note, hand them the pattern, not just the diagnosis.

What the dev did#

Sent the report. Waited. Got a message back that was, essentially, "thanks, that's terrifying, I'll fix it."

Two and a bit weeks later, a new APK landed in my downloads folder and a message saying "can you run your security check again."

I did. Same tooling, same commands, spent about an hour on it.

  • The old anon key was dead. New one baked in, extracted the same way.
  • The players table now returns an empty list to anyone using the anon key. RLS is on and the policy denies reads.
  • A new players_public view exists. Same 400+ rows, but every field with PII is gone. Just id, team, name, role, dates. Exactly the shape I'd suggested.
  • Every write test I could run came back blocked. UPDATE returns an empty array or an explicit RLS violation. INSERT gets a code: 42501, which is Postgres for "row violates row-level security policy". DELETE the same.
  • I probed for tables that a dev might have added between versions and left open (things like admin_users, push_tokens, messages) and found none.

That's a genuinely thorough fix. He didn't just patch the field I named, he applied the whole pattern across every table and added the view. Roughly the fastest, cleanest turnaround I've seen from a hobbyist dev on a real security issue.

The general lesson#

I keep coming back to two things.

The first is that Supabase is not the problem here. It's a perfectly good tool. The failure mode is that the security model it relies on is opt-in per table, and if you build an app in 2026 by iterating with AI, "enable RLS on every table" is not something the model will remind you to do unprompted. It'll help you write the migration when you ask, but it won't ask. Which means the modal state of the many, many small Supabase apps in the wild is probably identical to the one I looked at. RLS off by default, anon key doing the load-bearing, PII one curl away.

If you have shipped a Supabase-backed app in the last two years and you can't remember doing the RLS work explicitly, go and check. It'll take you five minutes. The bar for someone finding you is very low.

The second is that responsible disclosure works even at this scale. This wasn't a bug bounty programme, there was no formal process, the dev is a hobbyist not a company. All that happened was one person handed another person a clear, unemotional report with a concrete remediation pattern. Two weeks later the app was properly secured. Every step of that could have gone worse and none of them did, because the report was written in a way that made the fix feel achievable rather than shameful.

If you find something like this in an app of a friend, or a friend of a friend, that's the tone to hit. Not "look what I found." Just: "here's what's broken, here's why it matters, here's exactly what to do about it." The people who ship small apps for their local pool league or their five-a-side or their gaming club deserve that. So do the people who trusted them with a phone number.

Anyway, that's what I did with my evening. Full re-audit passed. Every finding fixed. My dad's mate can carry on running his league.