Missing Supabase Row Level Security policies in an AI-built app mean your database tables are effectively open to anyone holding the public key, because RLS is the control that decides what the anonymous and authenticated roles can do. AI builders like Lovable and Cursor generate working apps quickly and often create tables without enabling RLS or writing policies, so the auth constraints that should limit access are simply absent. The fix is direct: enable RLS on every table, then write policies that tie each row to the authenticated user, typically with a clause like auth.uid() = user_id. This takes only a few lines of SQL per table.
Short answer
Missing RLS means a table has no policies constraining the public key, so an AI-built app that skipped it exposes its data to anyone with that key. Per Supabase's Row Level Security guidance, RLS is what limits the anonymous and authenticated roles to what your policies allow, and AI-generated code frequently omits it. To fix a table, enable RLS with alter table your_table enable row level security, then add per-operation policies that tie rows to the user, using Supabase's auth.uid function in a clause like auth.uid() = user_id. Enable RLS first, since a table with it off is open, then write and test policies for select, insert, update, and delete.
What missing RLS means for your database
Missing RLS means there is no rule deciding what a request with the public key may read or write, so the table is governed only by the role's broad access rather than by any per-row restriction. In a Supabase app, the client connects with the public key as the anonymous or authenticated role, and without RLS constraining that role, the table is reachable through the auto-generated API by anyone who has the key, which is public by design.
The practical consequence is exposure. Someone can take the public key from your app, call the API directly, and read, modify, or delete records the interface never intended to show, because nothing at the database level stops them. This is why missing RLS is not a minor oversight but the core reason AI-built apps leak data: the app works because the key grants access, and that same access is available to everyone without policies to narrow it.
Why AI-generated code skips RLS
AI builders skip RLS because generating a working app and generating a secure one are different tasks, and the tools optimize for the first. To make the app function, the fastest path is to create tables and connect the client, and enabling Row Level Security with thoughtful policies is an extra step that is not required for the demo to run. So the generated project works while quietly lacking the access controls a security-minded developer would add.
This does mean AI code often skips the auth constraints. An assistant asked to build a feature will produce tables and queries that succeed with the public key, without necessarily tying rows to the logged-in user or restricting the anonymous role. The result is an app that appears to respect logins in its interface while the database underneath allows far more, which is exactly the gap that RLS policies close. Treat any generated backend as needing RLS added rather than assuming it is present.
How to enable RLS
Enabling RLS is a single statement per table, and it is the required first step. Run alter table your_table enable row level security for each table the client can reach. Once RLS is enabled, the table denies access to the anonymous and authenticated roles until you add policies, so enabling it immediately locks the table down rather than leaving it open.
This order matters. A table with RLS disabled is open to the role's access regardless of any policies you might write, so you must enable RLS before policies have any effect. After enabling it, your app may temporarily lose access to that table until you add the policies that grant the intended access, which is expected: you are moving from open-by-default to closed-by-default, then opening exactly what you mean to.
How to write RLS policies quickly
Writing policies is a few lines of SQL per table, and the key building block is Supabase's auth.uid function, which returns the current user's ID from their token. A typical policy lets users act only on their own rows by comparing auth.uid() to a user_id column. For reads, you write a policy such as: create policy "select own" on your_table for select using (auth.uid() = user_id). That clause allows a user to select only rows where the row's user_id matches their authenticated ID.
Write one policy per operation you need. Inserts use a with check clause, for example create policy "insert own" on your_table for insert with check (auth.uid() = user_id), which ensures a user can only create rows tied to themselves. Updates and deletes follow the same pattern with a using clause, and genuinely public data can use using (true) for read-only access. Because each policy is short and explicit, you can secure a table in minutes once you know the pattern.
Common policy patterns
Most tables need a small set of familiar policies. The table below shows common goals and the clause that achieves each.
| Goal | Policy clause |
|---|---|
| Users read their own rows | for select using (auth.uid() = user_id) |
| Users create their own rows | for insert with check (auth.uid() = user_id) |
| Users update their own rows | for update using (auth.uid() = user_id) |
| Public read-only data | for select using (true) |
| Deny all by default | RLS enabled with no matching policy |
Use the table as a starting set. Most user-owned tables need the first three, public reference data uses the fourth, and the fifth is simply what you get after enabling RLS before adding any policy, which is a safe default to build from rather than a state to leave in place.
Testing your policies
Adding policies is not enough; you must test that they actually restrict access, because a policy that is too permissive leaves you no safer than none. The checklist below covers securing and verifying a table.
| Check | Action | Done? |
|---|---|---|
| Enable RLS | Run enable row level security on every client table | [ ] |
| Per-operation policies | Add select, insert, update, and delete policies | [ ] |
| Tie to the user | Use auth.uid() = user_id where rows are user-owned | [ ] |
| Test as anonymous | Confirm the anon role cannot reach forbidden data | [ ] |
| Keep the secret key out | Ensure the service_role key stays server-side | [ ] |
The step people skip is testing as the anonymous role. Query your tables with the public key and confirm you can only reach what the policies intend, since that check is what proves the policy holds. Verifying access as an unauthenticated user turns a policy you wrote into protection you have confirmed.
Verify with a scan
Fixing RLS secures the backend, but the app side is worth checking too, because the same fast-build pattern that skipped RLS can also ship a secret key that bypasses RLS entirely. That secret key, the service_role or sb_secret key, is the dangerous one to leak, not the public key, and it should never appear in the app.
A scanner like PTKD.com analyzes your app build and reports findings ordered by severity and mapped to OWASP MASVS, including secrets embedded in the app, so you can confirm no secret key shipped alongside the public one. To be clear about the boundary: PTKD does not write your RLS policies or configure Supabase. It checks the built app for exposed secrets, which complements the RLS work that fixes the data exposure itself.
What to take away
- Missing RLS means no rule constrains the public key, so an AI-built app that skipped it exposes its tables to anyone with that key.
- AI builders like Lovable and Cursor optimize for a working app, so generated code often skips RLS and the auth constraints it enforces.
- Enable RLS first with enable row level security, which locks the table down until you add policies, then open exactly what you intend.
- Write short per-operation policies using auth.uid() = user_id for user-owned rows, and test them as the anonymous role to confirm they hold.
- Keep the service_role secret key server-side, and verify with PTKD.com that no secret key shipped in the app.




