AI-coded apps

    Is Row Level Security Necessary in Supabase?

    A Supabase table with Row Level Security disabled being read and deleted by anyone using the public anon key, next to a locked table with policies.

    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 stateThe anon key canResult
    Row Level Security off, client-exposedRead, write, and delete every rowFull exposure and possible data loss
    Row Level Security on, no policiesNothing, denied by defaultSafe but the app cannot use it yet
    Row Level Security on, user-scoped policiesOnly the rows a policy allowsCorrect and secure
    Server-only table, service_role accessNot reachable by clientsLower 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.

    StepActionDone?
    Enable on every exposed tableTurn Row Level Security on across your schema[ ]
    Write user-scoped policiesGrant access based on auth.uid()[ ]
    Deny by defaultAdd no allow-all policy using true[ ]
    Test with the anon keyConfirm you cannot reach others' data[ ]
    Keep service_role server-sideNever ship the secret key in the app[ ]
    Re-auditCheck 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.
    • #row level security
    • #supabase
    • #database security
    • #anon key
    • #owasp masvs

    Frequently asked questions

    Is Row Level Security required in Supabase?
    Yes, for any table your client app can reach with the anon key, which is the standard Supabase setup. The anon key is public and ships in your app, so anyone can use it to query your database, and Row Level Security is the only thing deciding what those queries may do. Without it, a client-exposed table is fully open. It is less critical only for tables accessed exclusively from your server with the service_role key, though Supabase still recommends enabling it there.
    What happens if I leave Row Level Security off?
    The table becomes fully open to anyone with your anon key, which is everyone, since the key is embedded in your app. Supabase applies no per-row restriction, so a request using the public key can read every row, insert new ones, update existing ones, and delete them, regardless of who the user is or whether they are logged in. Your private data becomes readable and your data becomes writable by any user, which is an unprotected database.
    Can users delete my database without Row Level Security?
    They can delete your data. A table without Row Level Security accepts a delete request made with the public anon key just as it accepts a read, so someone who infers your schema can issue deletes that empty your tables, potentially all rows. They may not drop the database administratively, but emptying your tables is the same catastrophe for most apps. Row Level Security lets you allow deletes only on a user's own rows, or none, rejecting a mass delete.
    Why does the anon key make Row Level Security necessary?
    Because Supabase apps talk to the database directly with the anon key, which is public by design and present in your client, so you cannot keep it secret. That model is secure only because Row Level Security is expected to constrain what the public key can do, acting as the lock on each room while the key is the open front door. Remove Row Level Security and the public key has unrestricted access to a database it was only meant to reach through policies.
    How do I turn on Row Level Security and write a policy?
    Enable Row Level Security on the table, after which it denies all access by default until you add policies, so the immediate effect is a locked-down table. Then write policies granting the access your app needs, commonly allowing users to read and write only their own rows by comparing an owner column to auth.uid(). Write separate policies per operation, grant nothing extra, and test with the anon key by trying to reach data you should not, confirming the database rejects it.
    I ran with Row Level Security off. What should I do?
    Act as though your data may have been accessed, since a public key on an open table leaves no barrier. Enable Row Level Security on the exposed tables immediately and add correct policies to stop further open access, then review your database and logs for unexpected reads, writes, or deletions, restoring from a backup if needed. Also confirm your service_role key never shipped in the app, and rotate it if there is any chance it did, since it bypasses Row Level Security entirely.

    Keep reading

    Scan your app in minutes

    Upload an APK, AAB, or IPA. PTKD returns an OWASP-aligned report with copy-paste fixes.

    Try PTKD free