Four things I find in every AI-generated backend
AI coding tools produce backends that run, pass a demo, and look production-ready. In security review after security review, the same four classes of finding keep showing up underneath that surface.
- Published
- 11 Jun 2026
- Updated
- 04 Aug 2026
- Reading time
- 5 min
- Tags
- security · AI · backend · Supabase · code review
I've spent a fair amount of time recently reviewing backends that were largely scaffolded by AI coding assistants — Supabase projects, generated APIs, the kind of stack a founder or a small team stands up in days instead of months. The tools are genuinely good at getting something working. What they're not consistently good at is getting something secure, and the gap between "works in the demo" and "safe with real user data" tends to show up in the same four places, project after project. None of what follows is about a specific client or a specific incident — it's the pattern across many reviews, which is exactly what makes it worth writing down.
1. Broken tenant isolation in generated RLS policies
Row-level security is the mechanism Postgres-backed platforms like Supabase give you to enforce "user A can only see user A's rows" at the database layer, independent of whatever the application code does. It's also one of the easiest things for a generated backend to get subtly wrong, because a policy can look correct, pass a manual smoke test with one test user, and still leak.
The most common failure is a policy that checks the shape of a request rather than genuine ownership — for example, filtering on a tenant_id column that's supplied by the client request itself rather than derived from the authenticated session. That's not tenant isolation, it's a filter the client can simply not apply, or apply with a different value, and see another tenant's rows. A close second is policies that are correctly written on the primary table but never propagated to a related table — comments, attachments, audit logs — that gets added later and inherits none of the isolation logic because nobody re-ran the policy design exercise for the new table.
The fix isn't complicated in principle: every policy should derive the tenant or owner identity from auth.uid() or an equivalent server-verified session claim, never from a client-supplied field, and every new table needs its own explicit policy rather than an assumption that it inherits protection from a table it references. The discipline that's missing isn't technical difficulty — it's that generated code rarely gets a second pass asking "what happens if the request lies about who it is."
2. Unprotected storage buckets
Object storage is the part of the stack that gets forgotten because it doesn't feel like "the backend" — it feels like a file host bolted onto the side. AI-generated setups frequently leave storage buckets public, or "public read" when the intent was "authenticated read," because the default in a lot of quick-start templates is permissive and nobody circles back to tighten it before real files start landing in it.
The failure mode here is quiet: a public bucket doesn't throw errors, doesn't fail tests, doesn't show up in a functional QA pass at all. It just means that anyone with a predictable or leaked object URL can pull a file — invoices, user uploads, exported reports, sometimes credentials or config files that got dropped in during development and never cleaned out. This is the finding class I'd flag as highest-severity-for-lowest-effort-to-fix: checking bucket policies takes minutes, and the blast radius of getting it wrong is every object in the bucket, not just one record.
3. Privilege escalation paths
Generated backends tend to implement role checks as a single gate — "is this user an admin, yes or no" — rather than as a set of specific, narrowly scoped permissions. That coarseness creates room for escalation paths that aren't obvious from reading any single endpoint in isolation. A common pattern: a user-update endpoint that lets an authenticated user modify their own profile also, without an explicit exclusion, accepts a role field in the same payload — because the endpoint was generated to handle "update user record" generically, and nobody separated "fields a user can change about themselves" from "fields only an admin can change about anyone."
Another recurring shape is an admin-only endpoint that's properly gated on the surface but calls an internal function or database role that itself has broader privileges than the endpoint's check implies — so a flaw anywhere in the call chain, not just at the entry point, can grant more access than intended. Reviewing for this means tracing what a request can actually reach once past the first gate, not just confirming the first gate exists.
4. Missing server-side authorization — client-side checks only
This is the most fundamental of the four, and the most common. A generated frontend will happily hide an admin panel behind a role check, disable a button for users without the right permission, or redirect unauthorized users away from a page — all of which is fine as UX, and all of which is worthless as security if the corresponding API endpoint doesn't independently enforce the same check. AI coding tools are good at producing this pattern because it's genuinely the majority of what "access control" looks like in a tutorial or a quick demo: the visible behavior is correct, so it looks done.
The tell is simple to check for and consistently absent: can you call the underlying API directly — with curl, with the browser devtools network tab, with any HTTP client — bypassing the UI entirely, and get data or perform an action the UI would have blocked? If yes, the authorization lives in the wrong layer. Every check that matters for security has to be enforced where the client can't route around it, which means the server, not the component tree.
Why this pattern repeats
None of these four are exotic. They're the same categories of finding that predate AI code generation by decades — tenant isolation, storage permissions, privilege boundaries, server-side enforcement. What's changed is the speed at which a backend with all four gaps can go from idea to something handling real user data, and the fact that a generated codebase looks complete in a way that used to only happen after a team had spent real time on it. Looking complete and being reviewed are different things, and the gap between them is exactly where these findings live.
If there's one habit that catches most of this before it ships: before trusting a generated backend with real data, read the authorization logic as if you were the attacker, not the user — trace what happens when a request lies about its identity, its role, or its tenant, and check whether the answer is "the database refuses" or "the application politely declines and the database goes along with it." The second answer is where all four of these findings come from.
Have a system that needs to work in production?
Tell me what's breaking — or what you're building.