Supabase without Row Level Security: why AI-built apps leak data
By Kike Gandia · Co-Founder & CEO, OSCP
If you built your application with Lovable, Bolt, v0 or Replit, its data most likely lives in Supabase or an equivalent service, and the browser talks to that database directly. It is a modern and perfectly valid architecture, but it rests entirely on one piece that has to be configured by hand: Row Level Security. Without it, the application keeps working just as well and the database is readable by anyone. This guide explains why that happens, how to check it in your own application, and what has to end up closed.
What Row Level Security is and why it is everything here
In a classic architecture, the browser talks to your server and your server talks to the database. The server decides what each user may see, and the database is never exposed.
In the architecture builders use there is no intermediate server: the browser queries the database directly, using a public key that travels inside the front end itself. That means the decision of "what may this user see" is no longer made by your code: it is made by the database, row by row. Row Level Security is the mechanism that does it.
Hence its criticality. It is not an optional hardening layer: it is the only access control that exists in that design. If it is disabled, the public key anyone can read in the browser is enough to request the full contents of your tables.
Why it so often ends up disabled
It is not carelessness, nor bad faith on the tools' part; it follows from how you build with them.
While developing, RLS gets in the way: it enables rules that block queries, and the generator does not always write them correctly first time. The convenient path is to leave it off so everything works, keep building, and "turn it on before launch". That last step rarely happens, because nothing reminds you it is missing: the application is finished, it works, and there is no symptom.
On top of that sits a common misunderstanding about the key. The key the front end uses is called public or anonymous, and whoever is building reasonably assumes that if it is public it grants access to nothing sensitive. But what limits its reach is not the key: it is the RLS policies. With no policies, the public key reads everything.
How to check whether your application is exposed
Three checks, from least to most conclusive. Run them against your own application, never against a third party's.
1. Find the connection. Open your application, go into the browser developer tools and look at the network tab. You will see requests to your database project address and the key it connects with. Those being visible is normal and expected; the problem is never that they are visible.
2. Query with no session. Sign out completely, or open a private window, and repeat a data query. If you still receive records, there is no access control: anyone can do the same.
3. Query from another account. Create a second account and check whether from it you can retrieve records created by the first. If you can, RLS is enabled but the policies do not check ownership, which is the most common mistake after not enabling it at all.
If the first check already returns data with no session, there is no need to continue: your database is open and it is worth closing today.
What has to end up closed, not merely enabled
Enabling RLS is the first step and it is not sufficient. What to verify, table by table:
- RLS enabled on every table, auxiliary ones included. A forgotten table usually holds precisely what links users to data.
- Policies that check ownership, not just authentication. The difference between "there is a session" and "this record belongs to whoever is asking" is the difference between being protected and not.
- Separate policies for reads and writes. Closing reads and leaving writes open is common, and it allows other people's data to be modified or deleted.
- Equivalent rules on file storage. Buckets have their own permission system and do not inherit table policies.
- The service key never in the front end. There is a second key with full privileges that bypasses RLS by design. If it reaches the browser, none of the above matters any more.
What to do when it has already leaked
If the checks above confirm the database has been reachable, closing the policies is necessary but does not settle the matter. The data may have been read, and you will not be able to prove it was not: access through the public path does not leave the same trace as an intrusion.
If personal data of EU residents is involved, you have to assess whether notification to the supervisory authority within 72 hours applies. The assessment belongs to the data controller, and it is worth documenting even when the conclusion is that notification does not apply: that documentation is what gets requested afterwards.
In parallel, rotate the keys, review whatever access logs the provider retains and check for anomalous writes. That is the part most often forgotten: almost everyone thinks about who could read, and almost nobody about who could write.
FAQ
Is this a flaw in Supabase or in Lovable?
In neither. Supabase documents RLS prominently and warns when a table is unprotected; the builders construct on that model and warn about it too. Access control is, by design, the responsibility of whoever publishes the application. What is true is that the design concentrates all the risk in a single configuration step, and that this step is easy to postpone indefinitely.
I use Firebase rather than Supabase, does this apply to me?
Yes, under another name. Firebase has Security Rules and the same underlying model: the client talks to the data directly and the rules are the only access control. Rules in test mode leave the database open for a period, and many applications never leave that mode. The checks in this guide are equivalent.
Can I enable RLS with the application already in production?
Yes, and you should, but do it carefully: enabling it with no policies blocks every query and the application stops working. The safe order is to write the policies first, test them against a copy environment, and only then enable table by table, verifying the application between each step. An hour of orderly work versus an outage in production.
Related service
Related content
- Is an app built with AI secure?
- The 7 most frequent flaws in AI-generated apps
- How to secure a SaaS application
- Source code audit