Yes, Row Level Security is necessary in Supabase for any table your client app can reach, because Supabase apps use a public anon key that ships in your app, and without Row Level Security that key can read, modify, and delete every row in the table. Row Level Security is the gate that decides what a request using the public key may do; with it off, there is effectively no gate. The only tables where it is less critical are ones accessed exclusively from your server with the service_role key and never exposed to clients, and even there Supabase recommends enabling it. If you are skipping Row Level Security to move faster, you are shipping an open database.
Short answer
Row Level Security is necessary whenever a table is reachable with your public anon key, which is the standard Supabase setup. Per Supabase's Row Level Security documentation, policies decide per row what a request can read or write, and the anon key is public by design. Per Supabase's API keys documentation, that key ships in your client, so anyone can use it to query your database. Without Row Level Security, those queries can read, change, or delete all your data. Enable it on every exposed table, write policies tied to the signed-in user, and keep the service_role key server-side.
What happens if you leave Row Level Security off?
If you leave Row Level Security off on a table that clients can reach, that table is fully open to anyone with your anon key, which is everyone, since the key is embedded in your app. With no Row Level Security, Supabase applies no per-row restriction, so a request using the public key can select every row, insert new rows, update existing ones, and delete them. Your private data becomes readable by any user, and your data becomes writable by any user, regardless of who they are or whether they are even logged in.
This is not a theoretical risk; it is the default consequence of disabling the one control that governs access. People discover it when they find their app's data has been read, scraped, altered, or wiped, having assumed that because the data was behind an API it was protected. It was not the API protecting it; it was supposed to be Row Level Security, and with that off there is nothing between the public key and the full contents of the table. Leaving it off to save time trades a few minutes of setup for an unprotected database.
Can users delete my database?
Yes, with Row Level Security off and your anon key public, a user can delete your data. A table without Row Level Security accepts a delete request made with the anon key just as it accepts a read, so someone who understands your schema, which they can infer, can issue deletes that remove your rows, potentially all of them.
This is the scenario behind the horror stories of an app losing all its records overnight. The public key that makes your app work is the same key an attacker uses, and without Row Level Security the database cannot tell the difference between your app's legitimate writes and a malicious mass delete. Row Level Security is what lets you say that a user may delete only their own rows, or none at all, so that a request to delete everything is rejected. Turning it on is how you make sure a user cannot wipe your data.
Why the anon key makes Row Level Security necessary
The reason Row Level Security is not optional is the anon key model that Supabase is built on. Your app talks to the database directly using the anon, or publishable, key, which is designed to be public and is present in your client, so you cannot keep it secret and you are not meant to. That design is fine and secure, but only because Row Level Security is expected to constrain what the public key can do. The key is the front door being open to everyone; Row Level Security is the lock on each room.
Remove Row Level Security and the model breaks, because now the public key has unrestricted access to a database it was only ever supposed to reach through policies. In the standard Supabase pattern, the client is the caller, the key is public, and Row Level Security is the mechanism that makes direct client access safe. Take it away and you have direct client access with no safety, which is why it is necessary rather than optional.
When it is strictly required versus less critical
Row Level Security is strictly required for every table in a schema exposed to the client, which for most Supabase apps means essentially all your data tables. If the anon key can reach the table through the API, it needs Row Level Security, full stop. There is no version of a client-exposed table that is safe without it, so treat enablement on these tables as mandatory rather than a judgment call.
It is less critical only for tables that are never exposed to clients and are accessed exclusively from your server using the service_role key, which bypasses Row Level Security anyway. Even for those, Supabase recommends enabling Row Level Security as defense in depth, so that a future accidental exposure does not open the table, and Supabase surfaces warnings for public tables that lack it. The safe rule is to enable Row Level Security on all your tables and write policies where clients need access, rather than trying to reason about which tables might be safe to leave open, because the cost of being wrong is an exposed database.
How to turn it on and write a policy
Enabling Row Level Security is quick, which is why skipping it is never worth the time saved. In Supabase, you enable Row Level Security on a table, after which the table denies all access by default until you add policies, so the immediate effect of turning it on is that the table is locked down. You then write policies that grant the specific access your app needs, typically tying access to the authenticated user.
A common pattern is to allow users to read and write only their own rows by comparing a row's owner column to auth.uid(), the identifier of the signed-in user, so each user reaches only their data. Write separate policies for the operations you intend to allow, and deliberately grant nothing you do not, so that, for example, deletes are only permitted on a user's own rows or not at all. Test the policies using the anon key by attempting to access data you should not be able to, and confirm the database rejects it.
If your data was already exposed: contain it
If you have been running with Row Level Security off, act as though your data may already have been accessed, because a public key on an open table leaves no barrier and no reliable way to know it was not. Enable Row Level Security on the exposed tables immediately and add correct policies, which stops further open access at once. Then review your database and any logs for signs of unexpected reads, writes, or deletions, and restore from a backup if data was altered or removed.
While containing the exposure, check the rest of your key setup, since an open database often comes with other shortcuts. Confirm that your service_role key has never been shipped in your app, and rotate it in the Supabase dashboard if there is any chance it was, because that key bypasses Row Level Security entirely and would be a far worse exposure than the anon key. Turning on Row Level Security stops the ongoing risk, and auditing your keys and data ensures the incident is fully contained rather than just paused.
Row Level Security necessity at a glance
Comparing table states shows why enablement is not optional. The table below lays them out.
| Table state | The anon key can | Result |
|---|---|---|
| Row Level Security off, client-exposed | Read, write, and delete every row | Full exposure and possible data loss |
| Row Level Security on, no policies | Nothing, denied by default | Safe but the app cannot use it yet |
| Row Level Security on, user-scoped policies | Only the rows a policy allows | Correct and secure |
| Server-only table, service_role access | Not reachable by clients | Lower risk, still recommended on |
Read the table by the first row: a client-exposed table without Row Level Security is fully open, which is exactly the state to never ship.
Enablement checklist
Working through these steps makes Row Level Security do its job. The checklist below covers them.
| Step | Action | Done? |
|---|---|---|
| Enable on every exposed table | Turn Row Level Security on across your schema | [ ] |
| Write user-scoped policies | Grant access based on auth.uid() | [ ] |
| Deny by default | Add no allow-all policy using true | [ ] |
| Test with the anon key | Confirm you cannot reach others' data | [ ] |
| Keep service_role server-side | Never ship the secret key in the app | [ ] |
| Re-audit | Check for any table still missing Row Level Security | [ ] |
The step that matters most is enabling Row Level Security on every exposed table, because a single client-reachable table left without it is an open door regardless of how well the others are secured.
Audit your keys in the shipped app
Row Level Security is configured in Supabase, but a related risk lives in the app you ship: a service_role key accidentally bundled in would bypass Row Level Security entirely, making even correct policies moot. So while you secure the database with policies, it is worth confirming what keys actually ship in your client.
A scanner like PTKD.com analyzes your app build and reports issues such as leaked keys and secrets, insecure data storage, and over-broad permissions by severity, mapped to OWASP MASVS, so a service_role key that wrongly reached the app is flagged, while the anon key is expected. To be clear about the boundary: PTKD does not check your Row Level Security policies, which live in Supabase, and it does not enable them for you. It confirms your shipped app does not carry a secret that would defeat the policies you set.
What to take away
- Row Level Security is necessary for any Supabase table your client can reach, because the public anon key ships in your app and, without it, can read, modify, and delete all your data.
- If you leave it off on a client-exposed table, that table is fully open to anyone, and yes, a user can delete your rows and effectively wipe your data.
- The anon key model is what makes Row Level Security necessary: direct client access is only safe when policies constrain the public key.
- Enable Row Level Security on every exposed table and write user-scoped policies tied to auth.uid(), granting only the access your app needs.
- Keep the service_role key server-side, and audit your shipped app with a tool like PTKD.com to ensure no secret key defeats your policies.




